From 1b216856517933580cd50bd1eb8c1923b73ee44a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Oct 2019 13:38:35 -0700 Subject: [PATCH 001/306] Version bump to 102.0.dev0" --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cac0386b812..e1e9757dd02 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 101 +MINOR_VERSION = 102 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From 160c201be16678c6aa2416a0be20898eb2d86fe8 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 23 Oct 2019 23:13:56 +0200 Subject: [PATCH 002/306] Bump songpal to fix a regression (#28115) The new release fixes a single regression from requests to aiohttp conversion. Some devices do not respond with the correct mimetype which was not enforced by requests but is enforced by aiohttp. Related PR https://github.com/rytilahti/python-songpal/pull/59 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 55b02b66a59..b090a90d719 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.11.1" + "python-songpal==0.11.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index eacaee7d927..eeb9d0cba20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1561,7 +1561,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.11.1 +python-songpal==0.11.2 # homeassistant.components.synologydsm python-synology==0.2.0 From 6a1501b59c4d65ae158213c6a69b65acef06a809 Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Thu, 24 Oct 2019 00:10:57 +0000 Subject: [PATCH 003/306] Cover all possible values for venstar operation_mode (#27949) * cover all possible values for operation_mode * Update climate.py * Update climate.py * Update climate.py mapped homeassistant constants to client ones so we don't have to check for both * black + pylint * move mode_map to __init__() * Update climate.py * Update climate.py --- homeassistant/components/venstar/climate.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 81afef97541..280e691337d 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -98,6 +98,11 @@ class VenstarThermostat(ClimateDevice): """Initialize the thermostat.""" self._client = client self._humidifier = humidifier + self._mode_map = { + HVAC_MODE_HEAT: self._client.MODE_HEAT, + HVAC_MODE_COOL: self._client.MODE_COOL, + HVAC_MODE_AUTO: self._client.MODE_AUTO, + } def update(self): """Update the data from the thermostat.""" @@ -266,20 +271,20 @@ class VenstarThermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set a new target temperature.""" set_temp = True - operation_mode = kwargs.get(ATTR_HVAC_MODE, self._client.mode) + operation_mode = kwargs.get(ATTR_HVAC_MODE) temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) temperature = kwargs.get(ATTR_TEMPERATURE) - if operation_mode != self._client.mode: - set_temp = self._set_operation_mode(operation_mode) + if operation_mode and self._mode_map.get(operation_mode) != self._client.mode: + set_temp = self._set_operation_mode(self._mode_map.get(operation_mode)) if set_temp: - if operation_mode == self._client.MODE_HEAT: + if self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_HEAT: success = self._client.set_setpoints(temperature, self._client.cooltemp) - elif operation_mode == self._client.MODE_COOL: + elif self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_COOL: success = self._client.set_setpoints(self._client.heattemp, temperature) - elif operation_mode == self._client.MODE_AUTO: + elif self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_AUTO: success = self._client.set_setpoints(temp_low, temp_high) else: success = False From 8c31afc31e3f33a30baa5e9817bb8fb8362d1263 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 24 Oct 2019 00:32:18 +0000 Subject: [PATCH 004/306] [ci skip] Translation update --- .../components/adguard/.translations/it.json | 2 + .../cert_expiry/.translations/it.json | 4 +- .../coolmaster/.translations/it.json | 23 ++++++++++++ .../components/glances/.translations/it.json | 37 +++++++++++++++++++ .../components/lock/.translations/it.json | 5 +++ .../opentherm_gw/.translations/it.json | 11 ++++++ .../components/sensor/.translations/it.json | 36 +++++++++--------- .../components/solarlog/.translations/it.json | 21 +++++++++++ .../transmission/.translations/it.json | 5 ++- 9 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/it.json create mode 100644 homeassistant/components/glances/.translations/it.json create mode 100644 homeassistant/components/solarlog/.translations/it.json diff --git a/homeassistant/components/adguard/.translations/it.json b/homeassistant/components/adguard/.translations/it.json index 57f81dc1d99..1b3ce014d90 100644 --- a/homeassistant/components/adguard/.translations/it.json +++ b/homeassistant/components/adguard/.translations/it.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo Hass.io AdGuard Home.", + "adguard_home_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}.", "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." }, diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json index 73749382dd9..d95b9cd84a1 100644 --- a/homeassistant/components/cert_expiry/.translations/it.json +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -4,10 +4,12 @@ "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata" }, "error": { + "certificate_error": "Il certificato non pu\u00f2 essere convalidato", "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", "connection_timeout": "Tempo scaduto collegandosi a questo host", "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", - "resolve_failed": "Questo host non pu\u00f2 essere risolto" + "resolve_failed": "Questo host non pu\u00f2 essere risolto", + "wrong_host": "Il certificato non corrisponde al nome host" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/it.json b/homeassistant/components/coolmaster/.translations/it.json new file mode 100644 index 00000000000..b543a10d32d --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Impossibile connettersi all'istanza CoolMasterNet. Controlla il tuo host.", + "no_units": "Impossibile trovare alcuna unit\u00e0 HVAC nell'host CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Supporta la modalit\u00e0 fresco", + "dry": "Supporta la modalit\u00e0 asciutto", + "fan_only": "Supporta la modalit\u00e0 solo ventilatore", + "heat": "Supporta la modalit\u00e0 di riscaldamento", + "heat_cool": "Supporta la modalit\u00e0 di riscaldamento/raffreddamento automatica", + "host": "Host", + "off": "Pu\u00f2 essere spento" + }, + "title": "Impostare i dettagli della connessione CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/it.json b/homeassistant/components/glances/.translations/it.json new file mode 100644 index 00000000000..5fbfba547d9 --- /dev/null +++ b/homeassistant/components/glances/.translations/it.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "L'host \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi all'host", + "wrong_version": "Versione non supportata (solo 2 o 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "ssl": "Utilizzare SSL/TLS per connettersi al sistema Glances", + "username": "Nome utente", + "verify_ssl": "Verificare la certificazione del sistema", + "version": "Glances API Version (2 o 3)" + }, + "title": "Impostare Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "description": "Configura le opzioni per Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/it.json b/homeassistant/components/lock/.translations/it.json index f56ef71060b..05f0db78cdf 100644 --- a/homeassistant/components/lock/.translations/it.json +++ b/homeassistant/components/lock/.translations/it.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Blocca {entity_name}", + "open": "Apri {entity_name}", + "unlock": "Sblocca {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} \u00e8 bloccato", "is_unlocked": "{entity_name} \u00e8 sbloccato" diff --git a/homeassistant/components/opentherm_gw/.translations/it.json b/homeassistant/components/opentherm_gw/.translations/it.json index 9c62686e190..73c3a8db970 100644 --- a/homeassistant/components/opentherm_gw/.translations/it.json +++ b/homeassistant/components/opentherm_gw/.translations/it.json @@ -19,5 +19,16 @@ } }, "title": "Gateway OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura del pavimento", + "precision": "Precisione" + }, + "description": "Opzioni per OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json index 07b20245c16..cb643bbdd29 100644 --- a/homeassistant/components/sensor/.translations/it.json +++ b/homeassistant/components/sensor/.translations/it.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "Livello della batteria di {entity_name}", - "is_humidity": "Umidit\u00e0 di {entity_name}", - "is_illuminance": "Illuminazione di {entity_name}", - "is_power": "Potenza di {entity_name}", - "is_pressure": "Pressione di {entity_name}", - "is_signal_strength": "Potenza del segnale di {entity_name}", - "is_temperature": "Temperatura di {entity_name}", - "is_timestamp": "Data di {entity_name}", - "is_value": "Valore di {entity_name}" + "is_battery_level": "Livello della batteria attuale di {entity_name}", + "is_humidity": "Umidit\u00e0 attuale di {entity_name}", + "is_illuminance": "Illuminazione attuale di {entity_name}", + "is_power": "Alimentazione attuale di {entity_name}", + "is_pressure": "Pressione attuale di {entity_name}", + "is_signal_strength": "Potenza del segnale attuale di {entity_name}", + "is_temperature": "Temperatura attuale di {entity_name}", + "is_timestamp": "Data e ora attuali di {nome_entit\u00e0}", + "is_value": "Valore attuale di {entity_name}" }, "trigger_type": { - "battery_level": "Livello della batteria di {entity_name}", - "humidity": "Umidit\u00e0 di {entity_name}", - "illuminance": "Illuminazione di {entity_name}", - "power": "Potenza di {entity_name}", - "pressure": "Pressione di {entity_name}", - "signal_strength": "Potenza del segnale di {entity_name}", - "temperature": "Temperatura di {entity_name}", - "timestamp": "Data di {entity_name}", - "value": "Valore di {entity_name}" + "battery_level": "variazioni del livello di batteria di {entity_name} ", + "humidity": "variazioni di umidit\u00e0 di {entity_name} ", + "illuminance": "variazioni dell'illuminazione di {entity_name} ", + "power": "variazioni di alimentazione di {entity_name}", + "pressure": "variazioni della pressione di {entity_name}", + "signal_strength": "variazioni della potenza del segnale di {entity_name} ", + "temperature": "variazioni di temperatura di {entity_name} ", + "timestamp": "variazioni di data e ora di {entity_name} ", + "value": "{entity_name} valori cambiati" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/it.json b/homeassistant/components/solarlog/.translations/it.json new file mode 100644 index 00000000000..65c13f052d3 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi, verifica l'indirizzo host" + }, + "step": { + "user": { + "data": { + "host": "Il nome host o l'indirizzo IP del dispositivo Solar-Log", + "name": "Il prefisso da utilizzare per i sensori Solar-Log" + }, + "title": "Definire la connessione Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/it.json b/homeassistant/components/transmission/.translations/it.json index 17a03b6dba1..a7c4c675856 100644 --- a/homeassistant/components/transmission/.translations/it.json +++ b/homeassistant/components/transmission/.translations/it.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "L'host \u00e8 gi\u00e0 configurato.", "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." }, "error": { "cannot_connect": "Impossibile connettersi all'host", + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", "wrong_credentials": "Nome utente o password non validi" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Frequenza di aggiornamento" }, - "description": "Configurare le opzioni per Trasmissione" + "description": "Configurare le opzioni per Trasmissione", + "title": "Configurare le opzioni per Transmission" } } } From dd9ca70e96465b903cffc5bd2ba693ff57eafcaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Thu, 24 Oct 2019 04:03:25 +0000 Subject: [PATCH 005/306] Add onvif local datetime support (#26656) * Update camera.py * Add onvif local datetime support * Correct capture TimeZone * Replace one line if-statement by if-block --- homeassistant/components/onvif/camera.py | 33 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index c73886c13c0..59ee8a8c7ee 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -192,8 +192,27 @@ class ONVIFHassCamera(Camera): try: system_date = dt_util.utcnow() device_time = await devicemgmt.GetSystemDateAndTime() - if device_time: + if not device_time: + _LOGGER.debug( + """Couldn't get camera '%s' date/time. + GetSystemDateAndTime() return null/empty""", + self._name, + ) + return + + if device_time.UTCDateTime: + tzone = dt_util.UTC cdate = device_time.UTCDateTime + else: + tzone = ( + dt_util.get_time_zone(device_time.TimeZone) + or dt_util.DEFAULT_TIME_ZONE, + ) + cdate = device_time.LocalDateTime + + if cdate is None: + _LOGGER.warning("Could not retrieve date/time on this camera") + else: cam_date = dt.datetime( cdate.Date.Year, cdate.Date.Month, @@ -202,11 +221,17 @@ class ONVIFHassCamera(Camera): cdate.Time.Minute, cdate.Time.Second, 0, - dt_util.UTC, + tzone, ) + cam_date_utc = cam_date.astimezone(dt_util.UTC) + + _LOGGER.debug("TimeZone for date/time: %s", tzone) + _LOGGER.debug("Camera date/time: %s", cam_date) + _LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc) + _LOGGER.debug("System date/time: %s", system_date) dt_diff = cam_date - system_date @@ -214,10 +239,10 @@ class ONVIFHassCamera(Camera): if dt_diff_seconds > 5: _LOGGER.warning( - "The date/time on the camera is '%s', " + "The date/time on the camera (UTC) is '%s', " "which is different from the system '%s', " "this could lead to authentication issues", - cam_date, + cam_date_utc, system_date, ) except ServerDisconnectedError as err: From d44de6dd2bb2853f9a9a61b8a51355c14b40b86b Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Thu, 24 Oct 2019 16:03:29 +0200 Subject: [PATCH 006/306] Fix Venstar formatting to restore clean CI (#28171) --- homeassistant/components/venstar/climate.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 280e691337d..9e5450addc5 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -280,11 +280,20 @@ class VenstarThermostat(ClimateDevice): set_temp = self._set_operation_mode(self._mode_map.get(operation_mode)) if set_temp: - if self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_HEAT: + if ( + self._mode_map.get(operation_mode, self._client.mode) + == self._client.MODE_HEAT + ): success = self._client.set_setpoints(temperature, self._client.cooltemp) - elif self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_COOL: + elif ( + self._mode_map.get(operation_mode, self._client.mode) + == self._client.MODE_COOL + ): success = self._client.set_setpoints(self._client.heattemp, temperature) - elif self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_AUTO: + elif ( + self._mode_map.get(operation_mode, self._client.mode) + == self._client.MODE_AUTO + ): success = self._client.set_setpoints(temp_low, temp_high) else: success = False From b1fcecd5268f6547bb5ad811c15866701c6f5b21 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Thu, 24 Oct 2019 17:59:25 +0200 Subject: [PATCH 007/306] Add Unifi Led (#27475) * Added Unifi Led * fixed manifest * fixed style issue * removed unused setting * added sugested changes. * fixed order * fixed settings that are required * Fix review issues * fix variable name that was too short * Testing something * Reverted to a previous version for testing * Reverted testing changes. --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/unifiled/__init__.py | 1 + homeassistant/components/unifiled/light.py | 105 ++++++++++++++++++ .../components/unifiled/manifest.json | 8 ++ requirements_all.txt | 3 + 6 files changed, 119 insertions(+) create mode 100644 homeassistant/components/unifiled/__init__.py create mode 100644 homeassistant/components/unifiled/light.py create mode 100644 homeassistant/components/unifiled/manifest.json diff --git a/.coveragerc b/.coveragerc index f97a7524a21..3350bc359af 100644 --- a/.coveragerc +++ b/.coveragerc @@ -719,6 +719,7 @@ omit = homeassistant/components/uber/sensor.py homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py + homeassistant/components/unifiled/* homeassistant/components/upcloud/* homeassistant/components/upnp/* homeassistant/components/upc_connect/* diff --git a/CODEOWNERS b/CODEOWNERS index eb29ee28915..809101a5271 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -311,6 +311,7 @@ homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 homeassistant/components/twilio_sms/* @robbiet480 homeassistant/components/unifi/* @kane610 +homeassistant/components/unifiled/* @florisvdk homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core diff --git a/homeassistant/components/unifiled/__init__.py b/homeassistant/components/unifiled/__init__.py new file mode 100644 index 00000000000..8543cc1a8fd --- /dev/null +++ b/homeassistant/components/unifiled/__init__.py @@ -0,0 +1 @@ +"""Unifi LED Lights integration.""" diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py new file mode 100644 index 00000000000..3dd1a8d5dc9 --- /dev/null +++ b/homeassistant/components/unifiled/light.py @@ -0,0 +1,105 @@ +"""Support for Unifi Led lights.""" +import logging + +from unifiled import unifiled +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + Light, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +# Validation of the user's configuration +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=20443): vol.All(cv.port, cv.string), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Unifi LED platform.""" + + # Assign configuration variables. + # The configuration check takes care they are present. + host = config[CONF_HOST] + port = config[CONF_PORT] + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + api = unifiled(host, port, username=username, password=password) + + # Verify that passed in configuration works + if not api.getloginstate(): + _LOGGER.error("Could not connect to unifiled controller") + return + + add_entities(UnifiLedLight(light, api) for light in api.getlights()) + + +class UnifiLedLight(Light): + """Representation of an unifiled Light.""" + + def __init__(self, light, api): + """Init Unifi LED Light.""" + + self._api = api + self._light = light + self._name = light["name"] + self._unique_id = light["id"] + self._state = light["status"]["output"] + self._brightness = self._api.convertfrom100to255(light["status"]["led"]) + self._features = SUPPORT_BRIGHTNESS + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def brightness(self): + """Return the brightness name of this light.""" + return self._brightness + + @property + def unique_id(self): + """Return the unique id of this light.""" + return self._unique_id + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + @property + def supported_features(self): + """Return the supported features of this light.""" + return self._features + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + self._api.setdevicebrightness( + self._unique_id, + str(self._api.convertfrom255to100(kwargs.get(ATTR_BRIGHTNESS, 255))), + ) + self._api.setdeviceoutput(self._unique_id, 1) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._api.setdeviceoutput(self._unique_id, 0) + + def update(self): + """Update the light states.""" + self._state = self._api.getlightstate(self._unique_id) + self._brightness = self._api.convertfrom100to255( + self._api.getlightbrightness(self._unique_id) + ) diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json new file mode 100644 index 00000000000..fbf05470c6d --- /dev/null +++ b/homeassistant/components/unifiled/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "unifiled", + "name": "Unifi LED", + "documentation": "https://www.home-assistant.io/integrations/unifiled", + "dependencies": [], + "codeowners": ["@florisvdk"], + "requirements": ["unifiled==0.10"] +} diff --git a/requirements_all.txt b/requirements_all.txt index eeb9d0cba20..0bb4afc2b84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1917,6 +1917,9 @@ twentemilieu==0.1.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.unifiled +unifiled==0.10 + # homeassistant.components.upcloud upcloud-api==0.4.3 From 969322e14a4db0a8edce8d9192e56b60b8f671c5 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 24 Oct 2019 12:23:02 -0400 Subject: [PATCH 008/306] Fixes/zha ieee tail (#28160) * Fix ZHA entity_id assignment. * Update tests. --- homeassistant/components/zha/entity.py | 2 +- tests/components/zha/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 00c3942358e..c11cd405a99 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -40,7 +40,7 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): self._unique_id = unique_id if not skip_entity_id: ieee = zha_device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) self.entity_id = "{}.{}_{}_{}_{}{}".format( self._domain, slugify(zha_device.manufacturer), diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 5f9172749b0..788faaaec73 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -168,7 +168,7 @@ def make_entity_id(domain, device, cluster, use_suffix=True): machine so that we can test state changes. """ ieee = device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) entity_id = "{}.{}_{}_{}_{}{}".format( domain, slugify(device.manufacturer), From fc09702cc33036b46d2b1566eebdabd6696bbd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 24 Oct 2019 19:31:49 +0300 Subject: [PATCH 009/306] Modernize Huawei LTE (#26675) * Modernization rework - config entry support, with override support from huawei_lte platform in YAML - device tracker entity registry support - refactor for easier addition of more features - internal code cleanups * Remove log level dependent subscription/data debug hack No longer needed, because pretty much all keys from supported categories are exposed as sensors. Closes https://github.com/home-assistant/home-assistant/issues/23819 * Upgrade huawei-lte-api to 1.4.1 https://github.com/Salamek/huawei-lte-api/releases * Add support for access without username and password * Use subclass init instead of config_entries.HANDLERS * Update huawei-lte-api to 1.4.3 (#27269) * Convert device state attributes to snake_case * Simplify scanner entity initialization * Remove not needed hass reference from Router * Return explicit None from unsupported old device tracker setup * Mark unknown connection errors during config as such * Drop some dead config flow code * Run config flow sync I/O in executor * Parametrize config flow login error tests * Forward entry unload to platforms * Async/sync fixups * Improve data subscription debug logging * Implement on the fly add of new and tracking of seen device tracker entities * Handle device tracker entry unload cleanup in component * Remove unnecessary _async_setup_lte, just have code in async_setup_entry * Remove time tracker on unload * Fix to not use same mutable default subscription set for all routers * Pylint fixes * Remove some redundant defensive device tracker code * Add back explicit get_scanner None return, hush pylint * Adjust approach to set system_options on entry create * Enable some sensors on first add instead of disabling everything * Fix SMS notification recipients default value * Add option to skip new device tracker entities * Fix SMS notification recipient option default * Work around https://github.com/PyCQA/pylint/issues/3202 * Remove unrelated type hint additions * Change async_add_new_entities to a regular function * Remove option to disable polling for new device tracker entries --- .../huawei_lte/.translations/en.json | 39 ++ .../components/huawei_lte/__init__.py | 429 ++++++++++++++---- .../components/huawei_lte/config_flow.py | 208 +++++++++ homeassistant/components/huawei_lte/const.py | 16 + .../components/huawei_lte/device_tracker.py | 181 ++++++-- .../components/huawei_lte/manifest.json | 5 +- homeassistant/components/huawei_lte/notify.py | 48 +- homeassistant/components/huawei_lte/sensor.py | 157 ++++--- .../components/huawei_lte/strings.json | 39 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- .../components/huawei_lte/test_config_flow.py | 140 ++++++ .../huawei_lte/test_device_tracker.py | 20 + tests/components/huawei_lte/test_init.py | 48 -- 15 files changed, 1067 insertions(+), 276 deletions(-) create mode 100644 homeassistant/components/huawei_lte/.translations/en.json create mode 100644 homeassistant/components/huawei_lte/config_flow.py create mode 100644 homeassistant/components/huawei_lte/strings.json create mode 100644 tests/components/huawei_lte/test_config_flow.py create mode 100644 tests/components/huawei_lte/test_device_tracker.py delete mode 100644 tests/components/huawei_lte/test_init.py diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json new file mode 100644 index 00000000000..8681e3355a4 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "This device is already configured" + }, + "error": { + "connection_failed": "Connection failed", + "incorrect_password": "Incorrect password", + "incorrect_username": "Incorrect username", + "incorrect_username_or_password": "Incorrect username or password", + "invalid_url": "Invalid URL", + "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", + "response_error": "Unknown error from device", + "unknown_connection_error": "Unknown error connecting to device" + }, + "step": { + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "User name" + }, + "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", + "title": "Configure Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "SMS notification recipients", + "track_new_devices": "Track new devices" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index f09788b7220..18f7035a885 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,34 +1,57 @@ """Support for Huawei LTE routers.""" +from collections import defaultdict from datetime import timedelta -from functools import reduce +from functools import partial from urllib.parse import urlparse import ipaddress import logging -import operator -from typing import Any, Callable +from typing import Any, Callable, Dict, List, Set import voluptuous as vol import attr from getmac import get_mac_address from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client -from huawei_lte_api.exceptions import ResponseErrorNotSupportedException +from huawei_lte_api.Connection import Connection +from huawei_lte_api.exceptions import ( + ResponseErrorLoginRequiredException, + ResponseErrorNotSupportedException, +) +from url_normalize import url_normalize +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT from homeassistant.const import ( + CONF_PASSWORD, + CONF_RECIPIENT, CONF_URL, CONF_USERNAME, - CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers import config_validation as cv -from homeassistant.util import Throttle +from homeassistant.core import CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, + dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType from .const import ( + ALL_KEYS, + DEFAULT_DEVICE_NAME, DOMAIN, + KEY_DEVICE_BASIC_INFORMATION, KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, + UPDATE_OPTIONS_SIGNAL, + UPDATE_SIGNAL, ) @@ -38,7 +61,20 @@ _LOGGER = logging.getLogger(__name__) # https://github.com/quandyfactory/dicttoxml/issues/60 logging.getLogger("dicttoxml").setLevel(logging.WARNING) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +DEFAULT_NAME_TEMPLATE = "Huawei {} {}" + +SCAN_INTERVAL = timedelta(seconds=10) + +NOTIFY_SCHEMA = vol.Any( + None, + vol.Schema( + { + vol.Optional(CONF_RECIPIENT): vol.Any( + None, vol.All(cv.ensure_list, [cv.string]) + ) + } + ), +) CONFIG_SCHEMA = vol.Schema( { @@ -48,8 +84,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Schema( { vol.Required(CONF_URL): cv.url, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(NOTIFY_DOMAIN): NOTIFY_SCHEMA, } ) ], @@ -60,97 +97,136 @@ CONFIG_SCHEMA = vol.Schema( @attr.s -class RouterData: +class Router: """Class for router state.""" - client = attr.ib() - mac = attr.ib() - device_information = attr.ib(init=False, factory=dict) - device_signal = attr.ib(init=False, factory=dict) - monitoring_traffic_statistics = attr.ib(init=False, factory=dict) - wlan_host_list = attr.ib(init=False, factory=dict) + connection: Connection = attr.ib() + url: str = attr.ib() + mac: str = attr.ib() + signal_update: CALLBACK_TYPE = attr.ib() - _subscriptions = attr.ib(init=False, factory=set) + data: Dict[str, Any] = attr.ib(init=False, factory=dict) + subscriptions: Dict[str, Set[str]] = attr.ib( + init=False, + factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)), + ) + unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list) + client: Client - def __getitem__(self, path: str): - """ - Get value corresponding to a dotted path. + def __attrs_post_init__(self): + """Set up internal state on init.""" + self.client = Client(self.connection) - The first path component designates a member of this class - such as device_information, device_signal etc, and the remaining - path points to a value in the member's data structure. - """ - root, *rest = path.split(".") - try: - data = getattr(self, root) - except AttributeError as err: - raise KeyError from err - return reduce(operator.getitem, rest, data) + @property + def device_name(self) -> str: + """Get router device name.""" + for key, item in ( + (KEY_DEVICE_BASIC_INFORMATION, "devicename"), + (KEY_DEVICE_INFORMATION, "DeviceName"), + ): + try: + return self.data[key][item] + except (KeyError, TypeError): + pass + return DEFAULT_DEVICE_NAME - def subscribe(self, path: str) -> None: - """Subscribe to given router data entries.""" - self._subscriptions.add(path.split(".")[0]) - - def unsubscribe(self, path: str) -> None: - """Unsubscribe from given router data entries.""" - self._subscriptions.discard(path.split(".")[0]) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self) -> None: - """Call API to update data.""" - self._update() + """Update router data.""" - def _update(self) -> None: - debugging = _LOGGER.isEnabledFor(logging.DEBUG) - - def get_data(path: str, func: Callable[[None], Any]) -> None: - if debugging or path in self._subscriptions: - try: - setattr(self, path, func()) - except ResponseErrorNotSupportedException: - _LOGGER.warning("%s not supported by device", path) - self._subscriptions.discard(path) - finally: - _LOGGER.debug("%s=%s", path, getattr(self, path)) + def get_data(key: str, func: Callable[[None], Any]) -> None: + if not self.subscriptions[key]: + return + _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) + try: + self.data[key] = func() + except ResponseErrorNotSupportedException: + _LOGGER.info( + "%s not supported by device, excluding from future updates", key + ) + self.subscriptions.pop(key) + except ResponseErrorLoginRequiredException: + _LOGGER.info( + "%s requires authorization, excluding from future updates", key + ) + self.subscriptions.pop(key) + finally: + _LOGGER.debug("%s=%s", key, self.data.get(key)) get_data(KEY_DEVICE_INFORMATION, self.client.device.information) + if self.data.get(KEY_DEVICE_INFORMATION): + # Full information includes everything in basic + self.subscriptions.pop(KEY_DEVICE_BASIC_INFORMATION, None) + get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) + self.signal_update() + + def cleanup(self, *_) -> None: + """Clean up resources.""" + + self.subscriptions.clear() + + for handler in self.unload_handlers: + handler() + self.unload_handlers.clear() + + if not isinstance(self.connection, AuthorizedConnection): + return + try: + self.client.user.logout() + except ResponseErrorNotSupportedException: + _LOGGER.debug("Logout not supported by device", exc_info=True) + except ResponseErrorLoginRequiredException: + _LOGGER.debug("Logout not supported when not logged in", exc_info=True) + except Exception: # pylint: disable=broad-except + _LOGGER.warning("Logout error", exc_info=True) + @attr.s class HuaweiLteData: """Shared state.""" - data = attr.ib(init=False, factory=dict) - - def get_data(self, config): - """Get the requested or the only data value.""" - if CONF_URL in config: - return self.data.get(config[CONF_URL]) - if len(self.data) == 1: - return next(iter(self.data.values())) - - return None + hass_config: dict = attr.ib() + # Our YAML config, keyed by router URL + config: Dict[str, Dict[str, Any]] = attr.ib() + routers: Dict[str, Router] = attr.ib(init=False, factory=dict) -def setup(hass, config) -> bool: - """Set up Huawei LTE component.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = HuaweiLteData() - for conf in config.get(DOMAIN, []): - _setup_lte(hass, conf) - return True +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: + """Set up Huawei LTE component from config entry.""" + url = config_entry.data[CONF_URL] - -def _setup_lte(hass, lte_config) -> None: - """Set up Huawei LTE router.""" - url = lte_config[CONF_URL] - username = lte_config[CONF_USERNAME] - password = lte_config[CONF_PASSWORD] + # Override settings from YAML config, but only if they're changed in it + # Old values are stored as *_from_yaml in the config entry + yaml_config = hass.data[DOMAIN].config.get(url) + if yaml_config: + # Config values + new_data = {} + for key in CONF_USERNAME, CONF_PASSWORD: + if key in yaml_config: + value = yaml_config[key] + if value != config_entry.data.get(f"{key}_from_yaml"): + new_data[f"{key}_from_yaml"] = value + new_data[key] = value + # Options + new_options = {} + yaml_recipient = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_RECIPIENT) + if yaml_recipient is not None and yaml_recipient != config_entry.options.get( + f"{CONF_RECIPIENT}_from_yaml" + ): + new_options[f"{CONF_RECIPIENT}_from_yaml"] = yaml_recipient + new_options[CONF_RECIPIENT] = yaml_recipient + # Update entry if overrides were found + if new_data or new_options: + hass.config_entries.async_update_entry( + config_entry, + data={**config_entry.data, **new_data}, + options={**config_entry.options, **new_options}, + ) # Get MAC address for use in unique ids. Being able to use something # from the API would be nice, but all of that seems to be available only @@ -164,19 +240,194 @@ def _setup_lte(hass, lte_config) -> None: mode = "ip" except ValueError: mode = "hostname" - mac = get_mac_address(**{mode: host}) + mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host})) - connection = AuthorizedConnection(url, username=username, password=password) - client = Client(connection) + def get_connection() -> Connection: + """ + Set up a connection. - data = RouterData(client, mac) - hass.data[DOMAIN].data[url] = data + Authorized one if username/pass specified (even if empty), unauthorized one otherwise. + """ + username = config_entry.data.get(CONF_USERNAME) + password = config_entry.data.get(CONF_PASSWORD) + if username or password: + connection = AuthorizedConnection(url, username=username, password=password) + else: + connection = Connection(url) + return connection - def cleanup(event): - """Clean up resources.""" - try: - client.user.logout() - except ResponseErrorNotSupportedException as ex: - _LOGGER.debug("Logout not supported by device", exc_info=ex) + def signal_update() -> None: + """Signal updates to data.""" + dispatcher_send(hass, UPDATE_SIGNAL, url) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + connection = await hass.async_add_executor_job(get_connection) + + # Set up router and store reference to it + router = Router(connection, url, mac, signal_update) + hass.data[DOMAIN].routers[url] = router + + # Do initial data update + await hass.async_add_executor_job(router.update) + + # Clear all subscriptions, enabled entities will push back theirs + router.subscriptions.clear() + + # Forward config entry setup to platforms + for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, domain) + ) + # Notify doesn't support config entry setup yet, load with discovery for now + await discovery.async_load_platform( + hass, + NOTIFY_DOMAIN, + DOMAIN, + {CONF_URL: url, CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT)}, + hass.data[DOMAIN].hass_config, + ) + + # Add config entry options update listener + router.unload_handlers.append( + config_entry.add_update_listener(async_signal_options_update) + ) + + def _update_router(*_: Any) -> None: + """ + Update router data. + + Separate passthrough function because lambdas don't work with track_time_interval. + """ + router.update() + + # Set up periodic update + router.unload_handlers.append( + async_track_time_interval(hass, _update_router, SCAN_INTERVAL) + ) + + # Clean up at end + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) + + return True + + +async def async_unload_entry( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> bool: + """Unload config entry.""" + + # Forward config entry unload to platforms + for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN): + await hass.config_entries.async_forward_entry_unload(config_entry, domain) + + # Forget about the router and invoke its cleanup + router = hass.data[DOMAIN].routers.pop(config_entry.data[CONF_URL]) + await hass.async_add_executor_job(router.cleanup) + + return True + + +async def async_setup(hass: HomeAssistantType, config) -> bool: + """Set up Huawei LTE component.""" + + # Arrange our YAML config to dict with normalized URLs as keys + domain_config = {} + if DOMAIN not in hass.data: + hass.data[DOMAIN] = HuaweiLteData(hass_config=config, config=domain_config) + for router_config in config.get(DOMAIN, []): + domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config + + for url, router_config in domain_config.items(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_URL: url, + CONF_USERNAME: router_config.get(CONF_USERNAME), + CONF_PASSWORD: router_config.get(CONF_PASSWORD), + }, + ) + ) + + return True + + +async def async_signal_options_update( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> None: + """Handle config entry options update.""" + async_dispatcher_send(hass, UPDATE_OPTIONS_SIGNAL, config_entry) + + +@attr.s +class HuaweiLteBaseEntity(Entity): + """Huawei LTE entity base class.""" + + router: Router = attr.ib() + + _available: bool = attr.ib(init=False, default=True) + _unsub_handlers: List[Callable] = attr.ib(init=False, factory=list) + + @property + def _entity_name(self) -> str: + raise NotImplementedError + + @property + def _device_unique_id(self) -> str: + """Return unique ID for entity within a router.""" + raise NotImplementedError + + @property + def unique_id(self) -> str: + """Return unique ID for entity.""" + return f"{self.router.mac}-{self._device_unique_id}" + + @property + def name(self) -> str: + """Return entity name.""" + return DEFAULT_NAME_TEMPLATE.format(self.router.device_name, self._entity_name) + + @property + def available(self) -> bool: + """Return whether the entity is available.""" + return self._available + + @property + def should_poll(self) -> bool: + """Huawei LTE entities report their state without polling.""" + return False + + async def async_update(self) -> None: + """Update state.""" + raise NotImplementedError + + async def async_update_options(self, config_entry: ConfigEntry) -> None: + """Update config entry options.""" + pass + + async def async_added_to_hass(self) -> None: + """Connect to update signals.""" + self._unsub_handlers.append( + async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self._async_maybe_update) + ) + self._unsub_handlers.append( + async_dispatcher_connect( + self.hass, UPDATE_OPTIONS_SIGNAL, self._async_maybe_update_options + ) + ) + + async def _async_maybe_update(self, url: str) -> None: + """Update state if the update signal comes from our router.""" + if url == self.router.url: + await self.async_update() + + async def _async_maybe_update_options(self, config_entry: ConfigEntry) -> None: + """Update options if the update signal comes from our router.""" + if config_entry.data[CONF_URL] == self.router.url: + await self.async_update_options(config_entry) + + async def async_will_remove_from_hass(self) -> None: + """Invoke unsubscription handlers.""" + for unsub in self._unsub_handlers: + unsub() + self._unsub_handlers.clear() diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py new file mode 100644 index 00000000000..52d586d088a --- /dev/null +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -0,0 +1,208 @@ +"""Config flow for the Huawei LTE platform.""" + +from collections import OrderedDict +import logging +from typing import Optional + +from huawei_lte_api.AuthorizedConnection import AuthorizedConnection +from huawei_lte_api.Client import Client +from huawei_lte_api.Connection import Connection +from huawei_lte_api.exceptions import ( + LoginErrorUsernameWrongException, + LoginErrorPasswordWrongException, + LoginErrorUsernamePasswordWrongException, + LoginErrorUsernamePasswordOverrunException, + ResponseErrorException, +) +from url_normalize import url_normalize +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME +from homeassistant.core import callback +from .const import DEFAULT_DEVICE_NAME + +# https://github.com/PyCQA/pylint/issues/3202 +from .const import DOMAIN # pylint: disable=unused-import + + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle Huawei LTE config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow.""" + return OptionsFlowHandler(config_entry) + + async def _async_show_user_form(self, user_input=None, errors=None): + if user_input is None: + user_input = {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + OrderedDict( + ( + ( + vol.Required( + CONF_URL, default=user_input.get(CONF_URL, "") + ), + str, + ), + ( + vol.Optional( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ), + str, + ), + ( + vol.Optional( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ), + str, + ), + ) + ) + ), + errors=errors or {}, + ) + + async def async_step_import(self, user_input=None): + """Handle import initiated config flow.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Handle user initiated config flow.""" + if user_input is None: + return await self._async_show_user_form() + + errors = {} + + # Normalize URL + user_input[CONF_URL] = url_normalize( + user_input[CONF_URL], default_scheme="http" + ) + if "://" not in user_input[CONF_URL]: + errors[CONF_URL] = "invalid_url" + return await self._async_show_user_form( + user_input=user_input, errors=errors + ) + + # See if we already have a router configured with this URL + existing_urls = { # existing entries + url_normalize(entry.data[CONF_URL], default_scheme="http") + for entry in self._async_current_entries() + } + if user_input[CONF_URL] in existing_urls: + return self.async_abort(reason="already_configured") + + conn = None + + def logout(): + if hasattr(conn, "user"): + try: + conn.user.logout() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not logout", exc_info=True) + + def try_connect(username: Optional[str], password: Optional[str]) -> Connection: + """Try connecting with given credentials.""" + if username or password: + conn = AuthorizedConnection( + user_input[CONF_URL], username=username, password=password + ) + else: + try: + conn = AuthorizedConnection( + user_input[CONF_URL], username="", password="" + ) + user_input[CONF_USERNAME] = "" + user_input[CONF_PASSWORD] = "" + except ResponseErrorException: + _LOGGER.debug( + "Could not login with empty credentials, proceeding unauthenticated", + exc_info=True, + ) + conn = Connection(user_input[CONF_URL]) + del user_input[CONF_USERNAME] + del user_input[CONF_PASSWORD] + return conn + + def get_router_title(conn: Connection) -> str: + """Get title for router.""" + title = None + client = Client(conn) + try: + info = client.device.basic_information() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not get device.basic_information", exc_info=True) + else: + title = info.get("devicename") + if not title: + try: + info = client.device.information() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not get device.information", exc_info=True) + else: + title = info.get("DeviceName") + return title or DEFAULT_DEVICE_NAME + + username = user_input.get(CONF_USERNAME) + password = user_input.get(CONF_PASSWORD) + try: + conn = await self.hass.async_add_executor_job( + try_connect, username, password + ) + except LoginErrorUsernameWrongException: + errors[CONF_USERNAME] = "incorrect_username" + except LoginErrorPasswordWrongException: + errors[CONF_PASSWORD] = "incorrect_password" + except LoginErrorUsernamePasswordWrongException: + errors[CONF_USERNAME] = "incorrect_username_or_password" + except LoginErrorUsernamePasswordOverrunException: + errors["base"] = "login_attempts_exceeded" + except ResponseErrorException: + _LOGGER.warning("Response error", exc_info=True) + errors["base"] = "response_error" + except Exception: # pylint: disable=broad-except + _LOGGER.warning("Unknown error connecting to device", exc_info=True) + errors[CONF_URL] = "unknown_connection_error" + if errors: + await self.hass.async_add_executor_job(logout) + return await self._async_show_user_form( + user_input=user_input, errors=errors + ) + + title = await self.hass.async_add_executor_job(get_router_title, conn) + await self.hass.async_add_executor_job(logout) + + return self.async_create_entry(title=title, data=user_input) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Huawei LTE options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_RECIPIENT, + default=self.config_entry.options.get(CONF_RECIPIENT, ""), + ): str + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 0134417d5fe..77126b61c22 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -2,7 +2,23 @@ DOMAIN = "huawei_lte" +DEFAULT_DEVICE_NAME = "LTE" + +UPDATE_SIGNAL = f"{DOMAIN}_update" +UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" + +KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_WLAN_HOST_LIST = "wlan_host_list" + +DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} + +SENSOR_KEYS = { + KEY_DEVICE_INFORMATION, + KEY_DEVICE_SIGNAL, + KEY_MONITORING_TRAFFIC_STATISTICS, +} + +ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index bad9253f4e7..d95d99e7126 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,63 +1,162 @@ """Support for device tracking of Huawei LTE routers.""" import logging -from typing import Any, Dict, List, Optional +import re +from typing import Any, Dict, Set import attr -import voluptuous as vol +from stringcase import snakecase -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, DeviceScanner +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, + SOURCE_TYPE_ROUTER, +) +from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.const import CONF_URL -from . import RouterData -from .const import DOMAIN, KEY_WLAN_HOST_LIST +from homeassistant.helpers import entity_registry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from . import HuaweiLteBaseEntity +from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_URL): cv.url}) - -HOSTS_PATH = f"{KEY_WLAN_HOST_LIST}.Hosts.Host" +_DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" -def get_scanner(hass, config): - """Get a Huawei LTE router scanner.""" - data = hass.data[DOMAIN].get_data(config) - data.subscribe(HOSTS_PATH) - return HuaweiLteScanner(data) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + + # Grab hosts list once to examine whether the initial fetch has got some data for + # us, i.e. if wlan host list is supported. Only set up a subscription and proceed + # with adding and tracking entities if it is. + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + try: + _ = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] + except KeyError: + _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") + return + + # Initialize already tracked entities + tracked: Set[str] = set() + registry = await entity_registry.async_get_registry(hass) + for entity in registry.entities.values(): + if ( + entity.domain == DEVICE_TRACKER_DOMAIN + and entity.config_entry_id == config_entry.entry_id + ): + tracked.add(entity.unique_id) + async_add_new_entities(hass, router.url, async_add_entities, tracked, True) + + # Tell parent router to poll hosts list to gather new devices + router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) + + async def _async_maybe_add_new_entities(url: str) -> None: + """Add new entities if the update signal comes from our router.""" + if url == router.url: + async_add_new_entities(hass, url, async_add_entities, tracked) + + # Register to handle router data updates + disconnect_dispatcher = async_dispatcher_connect( + hass, UPDATE_SIGNAL, _async_maybe_add_new_entities + ) + router.unload_handlers.append(disconnect_dispatcher) + + # Add new entities from initial scan + async_add_new_entities(hass, router.url, async_add_entities, tracked) + + +def async_add_new_entities( + hass, router_url, async_add_entities, tracked, included: bool = False +): + """Add new entities. + + :param included: if True, setup only items in tracked, and vice versa + """ + router = hass.data[DOMAIN].routers[router_url] + try: + hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] + except KeyError: + _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") + return + + new_entities = [] + for host in (x for x in hosts if x.get("MacAddress")): + entity = HuaweiLteScannerEntity(router, host["MacAddress"]) + tracking = entity.unique_id in tracked + if tracking != included: + continue + tracked.add(entity.unique_id) + new_entities.append(entity) + async_add_entities(new_entities, True) + + +def _better_snakecase(text: str) -> str: + if text == text.upper(): + # All uppercase to all lowercase to get http for HTTP, not h_t_t_p + text = text.lower() + else: + # Three or more consecutive uppercase with middle part lowercased + # to get http_response for HTTPResponse, not h_t_t_p_response + text = re.sub( + r"([A-Z])([A-Z]+)([A-Z](?:[^A-Z]|$))", + lambda match: f"{match.group(1)}{match.group(2).lower()}{match.group(3)}", + text, + ) + return snakecase(text) @attr.s -class HuaweiLteScanner(DeviceScanner): - """Huawei LTE router scanner.""" +class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): + """Huawei LTE router scanner entity.""" - data = attr.ib(type=RouterData) + mac: str = attr.ib() - _hosts = attr.ib(init=False, factory=dict) + _is_connected: bool = attr.ib(init=False, default=False) + _name: str = attr.ib(init=False, default="device") + _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict) - def scan_devices(self) -> List[str]: - """Scan for devices.""" - self.data.update() - try: - self._hosts = { - x["MacAddress"]: x for x in self.data[HOSTS_PATH] if x.get("MacAddress") + @property + def _entity_name(self) -> str: + return self._name + + @property + def _device_unique_id(self) -> str: + return self.mac + + @property + def source_type(self) -> str: + """Return SOURCE_TYPE_ROUTER.""" + return SOURCE_TYPE_ROUTER + + @property + def is_connected(self) -> bool: + """Get whether the entity is connected.""" + return self._is_connected + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Get additional attributes related to entity state.""" + return self._device_state_attributes + + async def async_update(self) -> None: + """Update state.""" + hosts = self.router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] + host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) + self._is_connected = host is not None + if self._is_connected: + self._name = host.get("HostName", self.mac) + self._device_state_attributes = { + _better_snakecase(k): v + for k, v in host.items() + if k not in ("MacAddress", "HostName") } - except KeyError: - _LOGGER.debug("%s not in data", HOSTS_PATH) - return list(self._hosts) - def get_device_name(self, device: str) -> Optional[str]: - """Get name for a device.""" - host = self._hosts.get(device) - return host.get("HostName") or None if host else None - def get_extra_attributes(self, device: str) -> Dict[str, Any]: - """ - Get extra attributes of a device. - - Some known extra attributes that may be returned in the dict - include MacAddress (MAC address), ID (client ID), IpAddress - (IP address), AssociatedSsid (associated SSID), AssociatedTime - (associated time in seconds), and HostName (host name). - """ - return self._hosts.get(device) or {} +def get_scanner(*args, **kwargs): # pylint: disable=useless-return + """Old no longer used way to set up Huawei LTE device tracker.""" + _LOGGER.warning( + "Loading and configuring as a platform is no longer supported or " + "required, convert to enabling/disabling available entities" + ) + return None diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 5d559cc60c5..b3c4442caa9 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -1,10 +1,13 @@ { "domain": "huawei_lte", "name": "Huawei LTE", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", - "huawei-lte-api==1.3.0" + "huawei-lte-api==1.4.3", + "stringcase==1.2.0", + "url-normalize==1.4.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index e882509c04c..4b5a63756b5 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,58 +1,54 @@ """Support for Huawei LTE router notifications.""" import logging +from typing import Any, List -import voluptuous as vol import attr +from huawei_lte_api.exceptions import ResponseErrorException -from homeassistant.components.notify import ( - BaseNotificationService, - ATTR_TARGET, - PLATFORM_SCHEMA, -) +from homeassistant.components.notify import BaseNotificationService, ATTR_TARGET from homeassistant.const import CONF_RECIPIENT, CONF_URL -import homeassistant.helpers.config_validation as cv +from . import Router from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_URL): cv.url, - vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]), - } -) - async def async_get_service(hass, config, discovery_info=None): """Get the notification service.""" - return HuaweiLteSmsNotificationService(hass, config) + if discovery_info is None: + _LOGGER.warning( + "Loading as a platform is no longer supported, convert to use " + "config entries or the huawei_lte component" + ) + return None + + router = hass.data[DOMAIN].routers[discovery_info[CONF_URL]] + default_targets = discovery_info[CONF_RECIPIENT] or [] + + return HuaweiLteSmsNotificationService(router, default_targets) @attr.s class HuaweiLteSmsNotificationService(BaseNotificationService): """Huawei LTE router SMS notification service.""" - hass = attr.ib() - config = attr.ib() + router: Router = attr.ib() + default_targets: List[str] = attr.ib() - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send message to target numbers.""" - from huawei_lte_api.exceptions import ResponseErrorException - targets = kwargs.get(ATTR_TARGET, self.config.get(CONF_RECIPIENT)) + targets = kwargs.get(ATTR_TARGET, self.default_targets) if not targets or not message: return - data = self.hass.data[DOMAIN].get_data(self.config) - if not data: - _LOGGER.error("Router not available") - return - try: - resp = data.client.sms.send_sms(phone_numbers=targets, message=message) + resp = self.router.client.sms.send_sms( + phone_numbers=targets, message=message + ) _LOGGER.debug("Sent to %s: %s", targets, resp) except ResponseErrorException as ex: _LOGGER.error("Could not send to %s: %s", targets, ex) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index cb8f5fb5766..e5b65c723f0 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -5,18 +5,15 @@ import re from typing import Optional import attr -import voluptuous as vol -from homeassistant.const import CONF_URL, CONF_MONITORED_CONDITIONS, STATE_UNKNOWN +from homeassistant.const import CONF_URL, STATE_UNKNOWN from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, DEVICE_CLASS_SIGNAL_STRENGTH, + DOMAIN as SENSOR_DOMAIN, ) from homeassistant.helpers import entity_registry -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv -from . import RouterData +from . import HuaweiLteBaseEntity from .const import ( DOMAIN, KEY_DEVICE_INFORMATION, @@ -27,34 +24,27 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME_TEMPLATE = "Huawei {} {}" -DEFAULT_DEVICE_NAME = "LTE" - -DEFAULT_SENSORS = [ - f"{KEY_DEVICE_INFORMATION}.WanIPAddress", - f"{KEY_DEVICE_SIGNAL}.rsrq", - f"{KEY_DEVICE_SIGNAL}.rsrp", - f"{KEY_DEVICE_SIGNAL}.rssi", - f"{KEY_DEVICE_SIGNAL}.sinr", -] SENSOR_META = { - f"{KEY_DEVICE_INFORMATION}.SoftwareVersion": dict(name="Software version"), - f"{KEY_DEVICE_INFORMATION}.WanIPAddress": dict( - name="WAN IP address", icon="mdi:ip" + KEY_DEVICE_INFORMATION: dict( + include=re.compile(r"^WanIP.*Address$", re.IGNORECASE) ), - f"{KEY_DEVICE_INFORMATION}.WanIPv6Address": dict( + (KEY_DEVICE_INFORMATION, "SoftwareVersion"): dict(name="Software version"), + (KEY_DEVICE_INFORMATION, "WanIPAddress"): dict( + name="WAN IP address", icon="mdi:ip", enabled_default=True + ), + (KEY_DEVICE_INFORMATION, "WanIPv6Address"): dict( name="WAN IPv6 address", icon="mdi:ip" ), - f"{KEY_DEVICE_SIGNAL}.band": dict(name="Band"), - f"{KEY_DEVICE_SIGNAL}.cell_id": dict(name="Cell ID"), - f"{KEY_DEVICE_SIGNAL}.lac": dict(name="LAC"), - f"{KEY_DEVICE_SIGNAL}.mode": dict( + (KEY_DEVICE_SIGNAL, "band"): dict(name="Band"), + (KEY_DEVICE_SIGNAL, "cell_id"): dict(name="Cell ID"), + (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC"), + (KEY_DEVICE_SIGNAL, "mode"): dict( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), ), - f"{KEY_DEVICE_SIGNAL}.pci": dict(name="PCI"), - f"{KEY_DEVICE_SIGNAL}.rsrq": dict( + (KEY_DEVICE_SIGNAL, "pci"): dict(name="PCI"), + (KEY_DEVICE_SIGNAL, "rsrq"): dict( name="RSRQ", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php @@ -65,8 +55,9 @@ SENSOR_META = { or x < -5 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", + enabled_default=True, ), - f"{KEY_DEVICE_SIGNAL}.rsrp": dict( + (KEY_DEVICE_SIGNAL, "rsrp"): dict( name="RSRP", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php @@ -77,8 +68,9 @@ SENSOR_META = { or x < -80 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", + enabled_default=True, ), - f"{KEY_DEVICE_SIGNAL}.rssi": dict( + (KEY_DEVICE_SIGNAL, "rssi"): dict( name="RSSI", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ @@ -89,8 +81,9 @@ SENSOR_META = { or x < -60 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", + enabled_default=True, ), - f"{KEY_DEVICE_SIGNAL}.sinr": dict( + (KEY_DEVICE_SIGNAL, "sinr"): dict( name="SINR", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php @@ -101,28 +94,38 @@ SENSOR_META = { or x < 10 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", + enabled_default=True, + ), + KEY_MONITORING_TRAFFIC_STATISTICS: dict( + exclude=re.compile(r"^showtraffic$", re.IGNORECASE) ), } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_URL): cv.url, - vol.Optional( - CONF_MONITORED_CONDITIONS, default=DEFAULT_SENSORS - ): cv.ensure_list, - } -) - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Huawei LTE sensor devices.""" - data = hass.data[DOMAIN].get_data(config) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] sensors = [] - for path in config.get(CONF_MONITORED_CONDITIONS): - if path == "traffic_statistics": # backwards compatibility - path = KEY_MONITORING_TRAFFIC_STATISTICS - data.subscribe(path) - sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {}))) + for key in ( + KEY_DEVICE_INFORMATION, + KEY_DEVICE_SIGNAL, + KEY_MONITORING_TRAFFIC_STATISTICS, + ): + items = router.data.get(key) + if not items: + continue + key_meta = SENSOR_META.get(key) + if key_meta: + include = key_meta.get("include") + if include: + items = filter(include.search, items) + exclude = key_meta.get("exclude") + if exclude: + items = [x for x in items if not exclude.search(x)] + for item in items: + sensors.append( + HuaweiLteSensor(router, key, item, SENSOR_META.get((key, item), {})) + ) # Pre-0.97 unique id migration. Old ones used the device serial number # (see comments in HuaweiLteData._setup_lte for more info), as well as @@ -134,7 +137,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if ent.platform != DOMAIN: continue for sensor in sensors: - oldsuf = ".".join(sensor.path) + oldsuf = ".".join(f"{sensor.key}.{sensor.item}") if ent.unique_id.endswith(f"_{oldsuf}"): entreg.async_update_entity(entid, new_unique_id=sensor.unique_id) _LOGGER.debug( @@ -162,30 +165,33 @@ def format_default(value): @attr.s -class HuaweiLteSensor(Entity): +class HuaweiLteSensor(HuaweiLteBaseEntity): """Huawei LTE sensor entity.""" - data = attr.ib(type=RouterData) - path = attr.ib(type=str) - meta = attr.ib(type=dict) + key: str = attr.ib() + item: str = attr.ib() + meta: dict = attr.ib() _state = attr.ib(init=False, default=STATE_UNKNOWN) - _unit = attr.ib(init=False, type=str) + _unit: str = attr.ib(init=False) + + async def async_added_to_hass(self): + """Subscribe to needed data on add.""" + await super().async_added_to_hass() + self.router.subscriptions[self.key].add(f"{SENSOR_DOMAIN}/{self.item}") + + async def async_will_remove_from_hass(self): + """Unsubscribe from needed data on remove.""" + await super().async_will_remove_from_hass() + self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") @property - def unique_id(self) -> str: - """Return unique ID for sensor.""" - return f"{self.data.mac}-{self.path}" + def _entity_name(self) -> str: + return self.meta.get("name", self.item) @property - def name(self) -> str: - """Return sensor name.""" - try: - dname = self.data[f"{KEY_DEVICE_INFORMATION}.DeviceName"] - except KeyError: - dname = None - vname = self.meta.get("name", self.path) - return DEFAULT_NAME_TEMPLATE.format(dname or DEFAULT_DEVICE_NAME, vname) + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" @property def state(self): @@ -210,18 +216,31 @@ class HuaweiLteSensor(Entity): return icon(self.state) return icon - def update(self): - """Update state.""" - self.data.update() + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return bool(self.meta.get("enabled_default")) + async def async_update(self): + """Update state.""" try: - value = self.data[self.path] + value = self.router.data[self.key][self.item] except KeyError: - _LOGGER.debug("%s not in data", self.path) - value = None + _LOGGER.debug("%s[%s] not in data", self.key, self.item) + self._available = False + return + self._available = True formatter = self.meta.get("formatter") if not callable(formatter): formatter = format_default self._state, self._unit = formatter(value) + + +async def async_setup_platform(*args, **kwargs): + """Old no longer used way to set up Huawei LTE sensors.""" + _LOGGER.warning( + "Loading and configuring as a platform is no longer supported or " + "required, convert to enabling/disabling available entities" + ) diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json new file mode 100644 index 00000000000..8681e3355a4 --- /dev/null +++ b/homeassistant/components/huawei_lte/strings.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "This device is already configured" + }, + "error": { + "connection_failed": "Connection failed", + "incorrect_password": "Incorrect password", + "incorrect_username": "Incorrect username", + "incorrect_username_or_password": "Incorrect username or password", + "invalid_url": "Invalid URL", + "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", + "response_error": "Unknown error from device", + "unknown_connection_error": "Unknown error connecting to device" + }, + "step": { + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "User name" + }, + "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", + "title": "Configure Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "SMS notification recipients", + "track_new_devices": "Track new devices" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4668528fedb..b694af1fb71 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -29,6 +29,7 @@ FLOWS = [ "heos", "homekit_controller", "homematicip_cloud", + "huawei_lte", "hue", "iaqualink", "ifttt", diff --git a/requirements_all.txt b/requirements_all.txt index 0bb4afc2b84..73cb1e8bf04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -665,7 +665,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.3.0 +huawei-lte-api==1.4.3 # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -1837,6 +1837,7 @@ steamodd==4.21 # homeassistant.components.streamlabswater streamlabswater==1.0.1 +# homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke # homeassistant.components.traccar @@ -1923,6 +1924,9 @@ unifiled==0.10 # homeassistant.components.upcloud upcloud-api==0.4.3 +# homeassistant.components.huawei_lte +url-normalize==1.4.1 + # homeassistant.components.uscis uscisstatus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39903b3606a..aa907170786 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -258,7 +258,7 @@ homematicip==0.10.13 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.3.0 +huawei-lte-api==1.4.3 # homeassistant.components.iaqualink iaqualink==0.2.9 @@ -581,6 +581,7 @@ sqlalchemy==1.3.10 # homeassistant.components.statsd statsd==3.2.1 +# homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke # homeassistant.components.traccar @@ -604,6 +605,9 @@ twentemilieu==0.1.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.huawei_lte +url-normalize==1.4.1 + # homeassistant.components.uvc uvcclient==0.11.0 diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py new file mode 100644 index 00000000000..aafa6abd57f --- /dev/null +++ b/tests/components/huawei_lte/test_config_flow.py @@ -0,0 +1,140 @@ +"""Tests for the Huawei LTE config flow.""" + +from huawei_lte_api.enums.client import ResponseCodeEnum +from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum +from requests_mock import ANY +from requests.exceptions import ConnectionError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_URL +from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler +from tests.common import MockConfigEntry + + +FIXTURE_USER_INPUT = { + CONF_URL: "http://192.168.1.1/", + CONF_USERNAME: "admin", + CONF_PASSWORD: "secret", +} + + +async def test_show_set_form(hass): + """Test that the setup form is served.""" + flow = ConfigFlowHandler() + 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"] == "user" + + +async def test_urlize_plain_host(hass, requests_mock): + """Test that plain host or IP gets converted to a URL.""" + requests_mock.request(ANY, ANY, exc=ConnectionError()) + flow = ConfigFlowHandler() + flow.hass = hass + host = "192.168.100.1" + user_input = {**FIXTURE_USER_INPUT, CONF_URL: host} + result = await flow.async_step_user(user_input=user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert user_input[CONF_URL] == f"http://{host}/" + + +async def test_already_configured(hass): + """Test we reject already configured devices.""" + MockConfigEntry( + domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Already configured" + ).add_to_hass(hass) + + flow = ConfigFlowHandler() + flow.hass = hass + # Tweak URL a bit to check that doesn't fail duplicate detection + result = await flow.async_step_user( + user_input={ + **FIXTURE_USER_INPUT, + CONF_URL: FIXTURE_USER_INPUT[CONF_URL].replace("http", "HTTP"), + } + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_connection_error(hass, requests_mock): + """Test we show user form on connection error.""" + + requests_mock.request(ANY, ANY, exc=ConnectionError()) + flow = ConfigFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_URL: "unknown_connection_error"} + + +@pytest.fixture +def login_requests_mock(requests_mock): + """Set up a requests_mock with base mocks for login tests.""" + requests_mock.request( + ANY, FIXTURE_USER_INPUT[CONF_URL], text='' + ) + requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/state-login", + text=( + f"{LoginStateEnum.LOGGED_OUT}" + f"{PasswordTypeEnum.SHA256}" + ), + ) + return requests_mock + + +@pytest.mark.parametrize( + ("code", "errors"), + ( + (LoginErrorEnum.USERNAME_WRONG, {CONF_USERNAME: "incorrect_username"}), + (LoginErrorEnum.PASSWORD_WRONG, {CONF_PASSWORD: "incorrect_password"}), + ( + LoginErrorEnum.USERNAME_PWD_WRONG, + {CONF_USERNAME: "incorrect_username_or_password"}, + ), + (LoginErrorEnum.USERNAME_PWD_ORERRUN, {"base": "login_attempts_exceeded"}), + (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}), + ), +) +async def test_login_error(hass, login_requests_mock, code, errors): + """Test we show user form with appropriate error on response failure.""" + login_requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", + text=f"{code}", + ) + flow = ConfigFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == errors + + +async def test_success(hass, login_requests_mock): + """Test successful flow provides entry creation data.""" + login_requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", + text=f"OK", + ) + flow = ConfigFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] + assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] diff --git a/tests/components/huawei_lte/test_device_tracker.py b/tests/components/huawei_lte/test_device_tracker.py new file mode 100644 index 00000000000..143fa5760e3 --- /dev/null +++ b/tests/components/huawei_lte/test_device_tracker.py @@ -0,0 +1,20 @@ +"""Huawei LTE device tracker tests.""" + +import pytest + +from homeassistant.components.huawei_lte import device_tracker + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + ("HTTP", "http"), + ("ID", "id"), + ("IPAddress", "ip_address"), + ("HTTPResponse", "http_response"), + ("foo_bar", "foo_bar"), + ), +) +def test_better_snakecase(value, expected): + """Test that better snakecase works better.""" + assert device_tracker._better_snakecase(value) == expected diff --git a/tests/components/huawei_lte/test_init.py b/tests/components/huawei_lte/test_init.py deleted file mode 100644 index e7323e1629e..00000000000 --- a/tests/components/huawei_lte/test_init.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Huawei LTE component tests.""" -from unittest.mock import Mock - -import pytest - -from homeassistant.components import huawei_lte -from homeassistant.components.huawei_lte.const import KEY_DEVICE_INFORMATION - - -@pytest.fixture(autouse=True) -def routerdata(): - """Set up a router data for testing.""" - rd = huawei_lte.RouterData(Mock(), "de:ad:be:ef:00:00") - rd.device_information = {"SoftwareVersion": "1.0", "nested": {"foo": "bar"}} - return rd - - -async def test_routerdata_get_nonexistent_root(routerdata): - """Test that accessing a nonexistent root element raises KeyError.""" - with pytest.raises(KeyError): # NOT AttributeError - routerdata["nonexistent_root.foo"] - - -async def test_routerdata_get_nonexistent_leaf(routerdata): - """Test that accessing a nonexistent leaf element raises KeyError.""" - with pytest.raises(KeyError): - routerdata[f"{KEY_DEVICE_INFORMATION}.foo"] - - -async def test_routerdata_get_nonexistent_leaf_path(routerdata): - """Test that accessing a nonexistent long path raises KeyError.""" - with pytest.raises(KeyError): - routerdata[f"{KEY_DEVICE_INFORMATION}.long.path.foo"] - - -async def test_routerdata_get_simple(routerdata): - """Test that accessing a short, simple path works.""" - assert routerdata[f"{KEY_DEVICE_INFORMATION}.SoftwareVersion"] == "1.0" - - -async def test_routerdata_get_longer(routerdata): - """Test that accessing a longer path works.""" - assert routerdata[f"{KEY_DEVICE_INFORMATION}.nested.foo"] == "bar" - - -async def test_routerdata_get_dict(routerdata): - """Test that returning an intermediate dict works.""" - assert routerdata[f"{KEY_DEVICE_INFORMATION}.nested"] == {"foo": "bar"} From 15bedd8f2797515eb086c61e9fd31e7afc096403 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Thu, 24 Oct 2019 09:41:04 -0700 Subject: [PATCH 010/306] Use latest withings_api module (#27817) * Using latest winthings_api module. Drastically reduced complexity of tests. * Removing import source. * Fixing test requirements. * Using requests_mock instead of responses module. * Updating file formatting. * Removing unused method. * Adding support for new OAuth2 config flow. * Addressing PR feedback. Removing unecessary base_url from config, this is a potential breaking change. * Addressing PR feedback. --- homeassistant/components/withings/__init__.py | 75 ++- homeassistant/components/withings/common.py | 191 ++++--- .../components/withings/config_flow.py | 210 ++----- homeassistant/components/withings/const.py | 35 +- .../components/withings/manifest.json | 2 +- homeassistant/components/withings/sensor.py | 262 ++++----- .../components/withings/strings.json | 10 +- requirements_all.txt | 2 +- requirements_test.txt | 1 + requirements_test_all.txt | 3 +- tests/components/withings/common.py | 534 ++++++++++++------ tests/components/withings/conftest.py | 350 ------------ tests/components/withings/test_common.py | 74 +-- tests/components/withings/test_config_flow.py | 162 ------ tests/components/withings/test_init.py | 349 +++++++++--- tests/components/withings/test_sensor.py | 310 ---------- 16 files changed, 998 insertions(+), 1572 deletions(-) delete mode 100644 tests/components/withings/conftest.py delete mode 100644 tests/components/withings/test_config_flow.py delete mode 100644 tests/components/withings/test_sensor.py diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index baed9300d46..482c4e96e5c 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -4,10 +4,11 @@ Support for the Withings API. For more details about this platform, please refer to the documentation at """ import voluptuous as vol +from withings_api import WithingsAuth -from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from . import config_flow, const from .common import _LOGGER, get_data_manager, NotAuthenticatedError @@ -22,7 +23,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(const.CLIENT_SECRET): vol.All( cv.string, vol.Length(min=1) ), - vol.Optional(const.BASE_URL): cv.url, vol.Required(const.PROFILES): vol.All( cv.ensure_list, vol.Unique(), @@ -36,50 +36,65 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the Withings component.""" - conf = config.get(DOMAIN) + conf = config.get(DOMAIN, {}) if not conf: return True hass.data[DOMAIN] = {const.CONFIG: conf} - base_url = conf.get(const.BASE_URL, hass.config.api.base_url).rstrip("/") - - hass.http.register_view(config_flow.WithingsAuthCallbackView) - - config_flow.register_flow_implementation( + config_flow.WithingsFlowHandler.async_register_implementation( hass, - conf[const.CLIENT_ID], - conf[const.CLIENT_SECRET], - base_url, - conf[const.PROFILES], - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={} - ) + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + const.DOMAIN, + conf[const.CLIENT_ID], + conf[const.CLIENT_SECRET], + f"{WithingsAuth.URL}/oauth2_user/authorize2", + f"{WithingsAuth.URL}/oauth2/token", + ), ) return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Withings from a config entry.""" - data_manager = get_data_manager(hass, entry) + # Upgrading existing token information to hass managed tokens. + if "auth_implementation" not in entry.data: + _LOGGER.debug("Upgrading existing config entry") + data = entry.data + creds = data.get(const.CREDENTIALS, {}) + hass.config_entries.async_update_entry( + entry, + data={ + "auth_implementation": const.DOMAIN, + "implementation": const.DOMAIN, + "profile": data.get("profile"), + "token": { + "access_token": creds.get("access_token"), + "refresh_token": creds.get("refresh_token"), + "expires_at": int(creds.get("token_expiry")), + "type": creds.get("token_type"), + "userid": creds.get("userid") or creds.get("user_id"), + }, + }, + ) + + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + + data_manager = get_data_manager(hass, entry, implementation) _LOGGER.debug("Confirming we're authenticated") try: await data_manager.check_authenticated() except NotAuthenticatedError: - # Trigger new config flow. - hass.async_create_task( - hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": SOURCE_USER, const.PROFILE: data_manager.profile}, - data={}, - ) + _LOGGER.error( + "Withings auth tokens exired for profile %s, remove and re-add the integration", + data_manager.profile, ) return False @@ -90,6 +105,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload Withings config entry.""" return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 9acca6f0cd6..911bb08906b 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -1,23 +1,36 @@ """Common code for Withings.""" import datetime +from functools import partial import logging import re import time +from typing import Any, Dict -import withings_api as withings -from oauthlib.oauth2.rfc6749.errors import MissingTokenError -from requests_oauthlib import TokenUpdated +from asyncio import run_coroutine_threadsafe +import requests +from withings_api import ( + AbstractWithingsApi, + SleepGetResponse, + MeasureGetMeasResponse, + SleepGetSummaryResponse, +) +from withings_api.common import UnauthorizedException, AuthFailedException from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.config_entry_oauth2_flow import ( + AbstractOAuth2Implementation, + OAuth2Session, +) from homeassistant.util import dt, slugify from . import const _LOGGER = logging.getLogger(const.LOG_NAMESPACE) NOT_AUTHENTICATED_ERROR = re.compile( - ".*(Error Code (100|101|102|200|401)|Missing access token parameter).*", + # ".*(Error Code (100|101|102|200|401)|Missing access token parameter).*", + "^401,.*", re.IGNORECASE, ) @@ -37,40 +50,82 @@ class ServiceError(HomeAssistantError): class ThrottleData: """Throttle data.""" - def __init__(self, interval: int, data): + def __init__(self, interval: int, data: Any): """Constructor.""" self._time = int(time.time()) self._interval = interval self._data = data @property - def time(self): + def time(self) -> int: """Get time created.""" return self._time @property - def interval(self): + def interval(self) -> int: """Get interval.""" return self._interval @property - def data(self): + def data(self) -> Any: """Get data.""" return self._data - def is_expired(self): + def is_expired(self) -> bool: """Is this data expired.""" return int(time.time()) - self.time > self.interval +class ConfigEntryWithingsApi(AbstractWithingsApi): + """Withing API that uses HA resources.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + implementation: AbstractOAuth2Implementation, + ): + """Initialize object.""" + self._hass = hass + self._config_entry = config_entry + self._implementation = implementation + self.session = OAuth2Session(hass, config_entry, implementation) + + def _request( + self, path: str, params: Dict[str, Any], method: str = "GET" + ) -> Dict[str, Any]: + return run_coroutine_threadsafe( + self.async_do_request(path, params, method), self._hass.loop + ).result() + + async def async_do_request( + self, path: str, params: Dict[str, Any], method: str = "GET" + ) -> Dict[str, Any]: + """Perform an async request.""" + await self.session.async_ensure_token_valid() + + response = await self._hass.async_add_executor_job( + partial( + requests.request, + method, + "%s/%s" % (self.URL, path), + params=params, + headers={ + "Authorization": "Bearer %s" + % self._config_entry.data["token"]["access_token"] + }, + ) + ) + + return response.json() + + class WithingsDataManager: """A class representing an Withings cloud service connection.""" service_available = None - def __init__( - self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi - ): + def __init__(self, hass: HomeAssistant, profile: str, api: ConfigEntryWithingsApi): """Constructor.""" self._hass = hass self._api = api @@ -95,27 +150,27 @@ class WithingsDataManager: return self._slug @property - def api(self): + def api(self) -> ConfigEntryWithingsApi: """Get the api object.""" return self._api @property - def measures(self): + def measures(self) -> MeasureGetMeasResponse: """Get the current measures data.""" return self._measures @property - def sleep(self): + def sleep(self) -> SleepGetResponse: """Get the current sleep data.""" return self._sleep @property - def sleep_summary(self): + def sleep_summary(self) -> SleepGetSummaryResponse: """Get the current sleep summary data.""" return self._sleep_summary @staticmethod - def get_throttle_interval(): + def get_throttle_interval() -> int: """Get the throttle interval.""" return const.THROTTLE_INTERVAL @@ -128,22 +183,26 @@ class WithingsDataManager: self.throttle_data[domain] = throttle_data @staticmethod - def print_service_unavailable(): + def print_service_unavailable() -> bool: """Print the service is unavailable (once) to the log.""" if WithingsDataManager.service_available is not False: _LOGGER.error("Looks like the service is not available at the moment") WithingsDataManager.service_available = False return True + return False + @staticmethod - def print_service_available(): + def print_service_available() -> bool: """Print the service is available (once) to to the log.""" if WithingsDataManager.service_available is not True: _LOGGER.info("Looks like the service is available again") WithingsDataManager.service_available = True return True - async def call(self, function, is_first_call=True, throttle_domain=None): + return False + + async def call(self, function, throttle_domain=None) -> Any: """Call an api method and handle the result.""" throttle_data = self.get_throttle_data(throttle_domain) @@ -167,21 +226,12 @@ class WithingsDataManager: WithingsDataManager.print_service_available() return result - except TokenUpdated: - WithingsDataManager.print_service_available() - if not is_first_call: - raise ServiceError( - "Stuck in a token update loop. This should never happen" - ) - - _LOGGER.info("Token updated, re-running call.") - return await self.call(function, False, throttle_domain) - - except MissingTokenError as ex: - raise NotAuthenticatedError(ex) - except Exception as ex: # pylint: disable=broad-except - # Service error, probably not authenticated. + # Withings api encountered error. + if isinstance(ex, (UnauthorizedException, AuthFailedException)): + raise NotAuthenticatedError(ex) + + # Oauth2 config flow failed to authenticate. if NOT_AUTHENTICATED_ERROR.match(str(ex)): raise NotAuthenticatedError(ex) @@ -189,37 +239,37 @@ class WithingsDataManager: WithingsDataManager.print_service_unavailable() raise PlatformNotReady(ex) - async def check_authenticated(self): + async def check_authenticated(self) -> bool: """Check if the user is authenticated.""" def function(): - return self._api.request("user", "getdevice", version="v2") + return bool(self._api.user_get_device()) return await self.call(function) - async def update_measures(self): + async def update_measures(self) -> MeasureGetMeasResponse: """Update the measures data.""" def function(): - return self._api.get_measures() + return self._api.measure_get_meas() self._measures = await self.call(function, throttle_domain="update_measures") return self._measures - async def update_sleep(self): + async def update_sleep(self) -> SleepGetResponse: """Update the sleep data.""" end_date = int(time.time()) start_date = end_date - (6 * 60 * 60) def function(): - return self._api.get_sleep(startdate=start_date, enddate=end_date) + return self._api.sleep_get(startdate=start_date, enddate=end_date) self._sleep = await self.call(function, throttle_domain="update_sleep") return self._sleep - async def update_sleep_summary(self): + async def update_sleep_summary(self) -> SleepGetSummaryResponse: """Update the sleep summary data.""" now = dt.utcnow() yesterday = now - datetime.timedelta(days=1) @@ -240,7 +290,7 @@ class WithingsDataManager: ) def function(): - return self._api.get_sleep_summary(lastupdate=yesterday_noon.timestamp()) + return self._api.sleep_get_summary(lastupdate=yesterday_noon) self._sleep_summary = await self.call( function, throttle_domain="update_sleep_summary" @@ -250,36 +300,16 @@ class WithingsDataManager: def create_withings_data_manager( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, + config_entry: ConfigEntry, + implementation: AbstractOAuth2Implementation, ) -> WithingsDataManager: """Set up the sensor config entry.""" - entry_creds = entry.data.get(const.CREDENTIALS) or {} - profile = entry.data[const.PROFILE] - credentials = withings.WithingsCredentials( - entry_creds.get("access_token"), - entry_creds.get("token_expiry"), - entry_creds.get("token_type"), - entry_creds.get("refresh_token"), - entry_creds.get("user_id"), - entry_creds.get("client_id"), - entry_creds.get("consumer_secret"), - ) - - def credentials_saver(credentials_param): - _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - - # Sanitizing the data as sometimes a WithingsCredentials object - # is passed through from the API. - cred_data = credentials_param - if not isinstance(credentials_param, dict): - cred_data = credentials_param.__dict__ - - entry.data[const.CREDENTIALS] = cred_data - hass.config_entries.async_update_entry(entry, data={**entry.data}) + profile = config_entry.data.get(const.PROFILE) _LOGGER.debug("Creating withings api instance") - api = withings.WithingsApi( - credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) + api = ConfigEntryWithingsApi( + hass=hass, config_entry=config_entry, implementation=implementation ) _LOGGER.debug("Creating withings data manager for profile: %s", profile) @@ -287,24 +317,25 @@ def create_withings_data_manager( def get_data_manager( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, + entry: ConfigEntry, + implementation: AbstractOAuth2Implementation, ) -> WithingsDataManager: """Get a data manager for a config entry. If the data manager doesn't exist yet, it will be created and cached for later use. """ - profile = entry.data.get(const.PROFILE) + entry_id = entry.entry_id - if not hass.data.get(const.DOMAIN): - hass.data[const.DOMAIN] = {} + hass.data[const.DOMAIN] = hass.data.get(const.DOMAIN, {}) - if not hass.data[const.DOMAIN].get(const.DATA_MANAGER): - hass.data[const.DOMAIN][const.DATA_MANAGER] = {} + domain_dict = hass.data[const.DOMAIN] + domain_dict[const.DATA_MANAGER] = domain_dict.get(const.DATA_MANAGER, {}) - if not hass.data[const.DOMAIN][const.DATA_MANAGER].get(profile): - hass.data[const.DOMAIN][const.DATA_MANAGER][ - profile - ] = create_withings_data_manager(hass, entry) + dm_dict = domain_dict[const.DATA_MANAGER] + dm_dict[entry_id] = dm_dict.get(entry_id) or create_withings_data_manager( + hass, entry, implementation + ) - return hass.data[const.DOMAIN][const.DATA_MANAGER][profile] + return dm_dict[entry_id] diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index c781e785f5e..cd1e4e4485d 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -1,192 +1,64 @@ """Config flow for Withings.""" -from collections import OrderedDict import logging -from typing import Optional -import aiohttp -import withings_api as withings import voluptuous as vol +from withings_api.common import AuthScope -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.http import HomeAssistantView -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback - -from . import const - -DATA_FLOW_IMPL = "withings_flow_implementation" +from homeassistant import config_entries +from homeassistant.components.withings import const +from homeassistant.helpers import config_entry_oauth2_flow _LOGGER = logging.getLogger(__name__) -@callback -def register_flow_implementation(hass, client_id, client_secret, base_url, profiles): - """Register a flow implementation. - - hass: Home assistant object. - client_id: Client id. - client_secret: Client secret. - base_url: Base url of home assistant instance. - profiles: The profiles to work with. - """ - if DATA_FLOW_IMPL not in hass.data: - hass.data[DATA_FLOW_IMPL] = OrderedDict() - - hass.data[DATA_FLOW_IMPL] = { - const.CLIENT_ID: client_id, - const.CLIENT_SECRET: client_secret, - const.BASE_URL: base_url, - const.PROFILES: profiles, - } - - @config_entries.HANDLERS.register(const.DOMAIN) -class WithingsFlowHandler(config_entries.ConfigFlow): +class WithingsFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): """Handle a config flow.""" - VERSION = 1 + DOMAIN = const.DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + _current_data = None - def __init__(self): - """Initialize flow.""" - self.flow_profile = None - self.data = None + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) - def async_profile_config_entry(self, profile: str) -> Optional[ConfigEntry]: - """Get a profile config entry.""" - entries = self.hass.config_entries.async_entries(const.DOMAIN) - for entry in entries: - if entry.data.get(const.PROFILE) == profile: - return entry + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return { + "scope": ",".join( + [ + AuthScope.USER_INFO.value, + AuthScope.USER_METRICS.value, + AuthScope.USER_ACTIVITY.value, + ] + ) + } - return None + async def async_oauth_create_entry(self, data: dict) -> dict: + """Override the create entry so user can select a profile.""" + self._current_data = data + return await self.async_step_profile(data) - def get_auth_client(self, profile: str): - """Get a new auth client.""" - flow = self.hass.data[DATA_FLOW_IMPL] - client_id = flow[const.CLIENT_ID] - client_secret = flow[const.CLIENT_SECRET] - base_url = flow[const.BASE_URL].rstrip("/") + async def async_step_profile(self, data: dict) -> dict: + """Prompt the user to select a user profile.""" + profile = data.get(const.PROFILE) - callback_uri = "{}/{}?flow_id={}&profile={}".format( - base_url.rstrip("/"), - const.AUTH_CALLBACK_PATH.lstrip("/"), - self.flow_id, - profile, - ) - - return withings.WithingsAuth( - client_id, - client_secret, - callback_uri, - scope=",".join(["user.info", "user.metrics", "user.activity"]), - ) - - async def async_step_import(self, user_input=None): - """Create user step.""" - return await self.async_step_user(user_input) - - async def async_step_user(self, user_input=None): - """Create an entry for selecting a profile.""" - flow = self.hass.data.get(DATA_FLOW_IMPL) - - if not flow: - return self.async_abort(reason="no_flows") - - if user_input: - return await self.async_step_auth(user_input) + if profile: + new_data = {**self._current_data, **{const.PROFILE: profile}} + self._current_data = None + return await self.async_step_finish(new_data) + profiles = self.hass.data[const.DOMAIN][const.CONFIG][const.PROFILES] return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - {vol.Required(const.PROFILE): vol.In(flow.get(const.PROFILES))} - ), + step_id="profile", + data_schema=vol.Schema({vol.Required(const.PROFILE): vol.In(profiles)}), ) - async def async_step_auth(self, user_input=None): - """Create an entry for auth.""" - if user_input.get(const.CODE): - self.data = user_input - return self.async_external_step_done(next_step_id="finish") + async def async_step_finish(self, data: dict) -> dict: + """Finish the flow.""" + self._current_data = None - profile = user_input.get(const.PROFILE) - - auth_client = self.get_auth_client(profile) - - url = auth_client.get_authorize_url() - - return self.async_external_step(step_id="auth", url=url) - - async def async_step_finish(self, user_input=None): - """Received code for authentication.""" - data = user_input or self.data or {} - - _LOGGER.debug( - "Should close all flows below %s", - self.hass.config_entries.flow.async_progress(), - ) - - profile = data[const.PROFILE] - code = data[const.CODE] - - return await self._async_create_session(profile, code) - - async def _async_create_session(self, profile, code): - """Create withings session and entries.""" - auth_client = self.get_auth_client(profile) - - _LOGGER.debug("Requesting credentials with code: %s.", code) - credentials = auth_client.get_credentials(code) - - return self.async_create_entry( - title=profile, - data={const.PROFILE: profile, const.CREDENTIALS: credentials.__dict__}, - ) - - -class WithingsAuthCallbackView(HomeAssistantView): - """Withings Authorization Callback View.""" - - requires_auth = False - url = const.AUTH_CALLBACK_PATH - name = const.AUTH_CALLBACK_NAME - - def __init__(self): - """Constructor.""" - - async def get(self, request): - """Receive authorization code.""" - hass = request.app["hass"] - - code = request.query.get("code") - profile = request.query.get("profile") - flow_id = request.query.get("flow_id") - - if not flow_id: - return aiohttp.web_response.Response( - status=400, text="'flow_id' argument not provided in url." - ) - - if not profile: - return aiohttp.web_response.Response( - status=400, text="'profile' argument not provided in url." - ) - - if not code: - return aiohttp.web_response.Response( - status=400, text="'code' argument not provided in url." - ) - - try: - await hass.config_entries.flow.async_configure( - flow_id, {const.PROFILE: profile, const.CODE: code} - ) - - return aiohttp.web_response.Response( - status=200, - headers={"content-type": "text/html"}, - text="", - ) - - except data_entry_flow.UnknownFlow: - return aiohttp.web_response.Response(status=400, text="Unknown flow") + return self.async_create_entry(title=data[const.PROFILE], data=data) diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index 79527d9d557..856f50ce9ad 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -19,6 +19,7 @@ AUTH_CALLBACK_PATH = "/api/withings/authorize" AUTH_CALLBACK_NAME = "withings:authorize" THROTTLE_INTERVAL = 60 +SCAN_INTERVAL = 60 STATE_UNKNOWN = const.STATE_UNKNOWN STATE_AWAKE = "awake" @@ -26,40 +27,6 @@ STATE_DEEP = "deep" STATE_LIGHT = "light" STATE_REM = "rem" -MEASURE_TYPE_BODY_TEMP = 71 -MEASURE_TYPE_BONE_MASS = 88 -MEASURE_TYPE_DIASTOLIC_BP = 9 -MEASURE_TYPE_FAT_MASS = 8 -MEASURE_TYPE_FAT_MASS_FREE = 5 -MEASURE_TYPE_FAT_RATIO = 6 -MEASURE_TYPE_HEART_PULSE = 11 -MEASURE_TYPE_HEIGHT = 4 -MEASURE_TYPE_HYDRATION = 77 -MEASURE_TYPE_MUSCLE_MASS = 76 -MEASURE_TYPE_PWV = 91 -MEASURE_TYPE_SKIN_TEMP = 73 -MEASURE_TYPE_SLEEP_DEEP_DURATION = "deepsleepduration" -MEASURE_TYPE_SLEEP_HEART_RATE_AVERAGE = "hr_average" -MEASURE_TYPE_SLEEP_HEART_RATE_MAX = "hr_max" -MEASURE_TYPE_SLEEP_HEART_RATE_MIN = "hr_min" -MEASURE_TYPE_SLEEP_LIGHT_DURATION = "lightsleepduration" -MEASURE_TYPE_SLEEP_REM_DURATION = "remsleepduration" -MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_AVERAGE = "rr_average" -MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MAX = "rr_max" -MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MIN = "rr_min" -MEASURE_TYPE_SLEEP_STATE_AWAKE = 0 -MEASURE_TYPE_SLEEP_STATE_DEEP = 2 -MEASURE_TYPE_SLEEP_STATE_LIGHT = 1 -MEASURE_TYPE_SLEEP_STATE_REM = 3 -MEASURE_TYPE_SLEEP_TOSLEEP_DURATION = "durationtosleep" -MEASURE_TYPE_SLEEP_TOWAKEUP_DURATION = "durationtowakeup" -MEASURE_TYPE_SLEEP_WAKEUP_DURATION = "wakeupduration" -MEASURE_TYPE_SLEEP_WAKUP_COUNT = "wakeupcount" -MEASURE_TYPE_SPO2 = 54 -MEASURE_TYPE_SYSTOLIC_BP = 10 -MEASURE_TYPE_TEMP = 12 -MEASURE_TYPE_WEIGHT = 1 - MEAS_BODY_TEMP_C = "body_temperature_c" MEAS_BONE_MASS_KG = "bone_mass_kg" MEAS_DIASTOLIC_MMHG = "diastolic_blood_pressure_mmhg" diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 7c6e4ec044a..ea9845f3e42 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "withings-api==2.0.0b8" + "withings-api==2.1.2" ], "dependencies": [ "api", diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 0293784fd3e..17eae93ec0d 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -1,10 +1,22 @@ """Sensors flow for Withings.""" -import typing as types +from typing import Callable, List, Union + +from withings_api.common import ( + MeasureType, + GetSleepSummaryField, + MeasureGetMeasResponse, + SleepGetResponse, + SleepGetSummaryResponse, + get_measure_value, + MeasureGroupAttribs, + SleepState, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify +from homeassistant.helpers import config_entry_oauth2_flow from . import const from .common import _LOGGER, WithingsDataManager, get_data_manager @@ -16,57 +28,22 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: types.Callable[[types.List[Entity], bool], None], -): + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: """Set up the sensor config entry.""" - data_manager = get_data_manager(hass, entry) - entities = create_sensor_entities(data_manager) + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + + data_manager = get_data_manager(hass, entry, implementation) + user_id = entry.data["token"]["userid"] + + entities = create_sensor_entities(data_manager, user_id) async_add_entities(entities, True) -def get_measures(): - """Get all the measures. - - This function exists to be easily mockable so we can test - one measure at a time. This becomes necessary when integration - testing throttle functionality in the data manager. - """ - return list(WITHINGS_MEASUREMENTS_MAP) - - -def create_sensor_entities(data_manager: WithingsDataManager): - """Create sensor entities.""" - entities = [] - - measures = get_measures() - - for attribute in WITHINGS_ATTRIBUTES: - if attribute.measurement not in measures: - _LOGGER.debug( - "Skipping measurement %s as it is not in the" - "list of measurements to use", - attribute.measurement, - ) - continue - - _LOGGER.debug( - "Creating entity for measurement: %s, measure_type: %s," - "friendly_name: %s, unit_of_measurement: %s", - attribute.measurement, - attribute.measure_type, - attribute.friendly_name, - attribute.unit_of_measurement, - ) - - entity = WithingsHealthSensor(data_manager, attribute) - - entities.append(entity) - - return entities - - class WithingsAttribute: """Base class for modeling withing data.""" @@ -107,104 +84,104 @@ class WithingsSleepSummaryAttribute(WithingsAttribute): WITHINGS_ATTRIBUTES = [ WithingsMeasureAttribute( const.MEAS_WEIGHT_KG, - const.MEASURE_TYPE_WEIGHT, + MeasureType.WEIGHT, "Weight", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_FAT_MASS_KG, - const.MEASURE_TYPE_FAT_MASS, + MeasureType.FAT_MASS_WEIGHT, "Fat Mass", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_FAT_FREE_MASS_KG, - const.MEASURE_TYPE_FAT_MASS_FREE, + MeasureType.FAT_FREE_MASS, "Fat Free Mass", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_MUSCLE_MASS_KG, - const.MEASURE_TYPE_MUSCLE_MASS, + MeasureType.MUSCLE_MASS, "Muscle Mass", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_BONE_MASS_KG, - const.MEASURE_TYPE_BONE_MASS, + MeasureType.BONE_MASS, "Bone Mass", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_HEIGHT_M, - const.MEASURE_TYPE_HEIGHT, + MeasureType.HEIGHT, "Height", const.UOM_LENGTH_M, "mdi:ruler", ), WithingsMeasureAttribute( const.MEAS_TEMP_C, - const.MEASURE_TYPE_TEMP, + MeasureType.TEMPERATURE, "Temperature", const.UOM_TEMP_C, "mdi:thermometer", ), WithingsMeasureAttribute( const.MEAS_BODY_TEMP_C, - const.MEASURE_TYPE_BODY_TEMP, + MeasureType.BODY_TEMPERATURE, "Body Temperature", const.UOM_TEMP_C, "mdi:thermometer", ), WithingsMeasureAttribute( const.MEAS_SKIN_TEMP_C, - const.MEASURE_TYPE_SKIN_TEMP, + MeasureType.SKIN_TEMPERATURE, "Skin Temperature", const.UOM_TEMP_C, "mdi:thermometer", ), WithingsMeasureAttribute( const.MEAS_FAT_RATIO_PCT, - const.MEASURE_TYPE_FAT_RATIO, + MeasureType.FAT_RATIO, "Fat Ratio", const.UOM_PERCENT, None, ), WithingsMeasureAttribute( const.MEAS_DIASTOLIC_MMHG, - const.MEASURE_TYPE_DIASTOLIC_BP, + MeasureType.DIASTOLIC_BLOOD_PRESSURE, "Diastolic Blood Pressure", const.UOM_MMHG, None, ), WithingsMeasureAttribute( const.MEAS_SYSTOLIC_MMGH, - const.MEASURE_TYPE_SYSTOLIC_BP, + MeasureType.SYSTOLIC_BLOOD_PRESSURE, "Systolic Blood Pressure", const.UOM_MMHG, None, ), WithingsMeasureAttribute( const.MEAS_HEART_PULSE_BPM, - const.MEASURE_TYPE_HEART_PULSE, + MeasureType.HEART_RATE, "Heart Pulse", const.UOM_BEATS_PER_MINUTE, "mdi:heart-pulse", ), WithingsMeasureAttribute( - const.MEAS_SPO2_PCT, const.MEASURE_TYPE_SPO2, "SP02", const.UOM_PERCENT, None + const.MEAS_SPO2_PCT, MeasureType.SP02, "SP02", const.UOM_PERCENT, None ), WithingsMeasureAttribute( - const.MEAS_HYDRATION, const.MEASURE_TYPE_HYDRATION, "Hydration", "", "mdi:water" + const.MEAS_HYDRATION, MeasureType.HYDRATION, "Hydration", "", "mdi:water" ), WithingsMeasureAttribute( const.MEAS_PWV, - const.MEASURE_TYPE_PWV, + MeasureType.PULSE_WAVE_VELOCITY, "Pulse Wave Velocity", const.UOM_METERS_PER_SECOND, None, @@ -214,91 +191,91 @@ WITHINGS_ATTRIBUTES = [ ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_WAKEUP_DURATION, + GetSleepSummaryField.WAKEUP_DURATION.value, "Wakeup time", const.UOM_SECONDS, "mdi:sleep-off", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_LIGHT_DURATION, + GetSleepSummaryField.LIGHT_SLEEP_DURATION.value, "Light sleep", const.UOM_SECONDS, "mdi:sleep", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_DEEP_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_DEEP_DURATION, + GetSleepSummaryField.DEEP_SLEEP_DURATION.value, "Deep sleep", const.UOM_SECONDS, "mdi:sleep", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_REM_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_REM_DURATION, + GetSleepSummaryField.REM_SLEEP_DURATION.value, "REM sleep", const.UOM_SECONDS, "mdi:sleep", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_WAKEUP_COUNT, - const.MEASURE_TYPE_SLEEP_WAKUP_COUNT, + GetSleepSummaryField.WAKEUP_COUNT.value, "Wakeup count", const.UOM_FREQUENCY, "mdi:sleep-off", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_TOSLEEP_DURATION, + GetSleepSummaryField.DURATION_TO_SLEEP.value, "Time to sleep", const.UOM_SECONDS, "mdi:sleep", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_TOWAKEUP_DURATION, + GetSleepSummaryField.DURATION_TO_WAKEUP.value, "Time to wakeup", const.UOM_SECONDS, "mdi:sleep-off", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_HEART_RATE_AVERAGE, - const.MEASURE_TYPE_SLEEP_HEART_RATE_AVERAGE, + GetSleepSummaryField.HR_AVERAGE.value, "Average heart rate", const.UOM_BEATS_PER_MINUTE, "mdi:heart-pulse", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_HEART_RATE_MIN, - const.MEASURE_TYPE_SLEEP_HEART_RATE_MIN, + GetSleepSummaryField.HR_MIN.value, "Minimum heart rate", const.UOM_BEATS_PER_MINUTE, "mdi:heart-pulse", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_HEART_RATE_MAX, - const.MEASURE_TYPE_SLEEP_HEART_RATE_MAX, + GetSleepSummaryField.HR_MAX.value, "Maximum heart rate", const.UOM_BEATS_PER_MINUTE, "mdi:heart-pulse", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, - const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_AVERAGE, + GetSleepSummaryField.RR_AVERAGE.value, "Average respiratory rate", const.UOM_BREATHS_PER_MINUTE, None, ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, - const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MIN, + GetSleepSummaryField.RR_MIN.value, "Minimum respiratory rate", const.UOM_BREATHS_PER_MINUTE, None, ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, - const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MAX, + GetSleepSummaryField.RR_MAX.value, "Maximum respiratory rate", const.UOM_BREATHS_PER_MINUTE, None, @@ -312,7 +289,10 @@ class WithingsHealthSensor(Entity): """Implementation of a Withings sensor.""" def __init__( - self, data_manager: WithingsDataManager, attribute: WithingsAttribute + self, + data_manager: WithingsDataManager, + attribute: WithingsAttribute, + user_id: str, ) -> None: """Initialize the Withings sensor.""" self._data_manager = data_manager @@ -320,7 +300,7 @@ class WithingsHealthSensor(Entity): self._state = None self._slug = self._data_manager.slug - self._user_id = self._data_manager.api.get_credentials().user_id + self._user_id = user_id @property def name(self) -> str: @@ -335,7 +315,7 @@ class WithingsHealthSensor(Entity): ) @property - def state(self): + def state(self) -> Union[str, int, float, None]: """Return the state of the sensor.""" return self._state @@ -350,7 +330,7 @@ class WithingsHealthSensor(Entity): return self._attribute.icon @property - def device_state_attributes(self): + def device_state_attributes(self) -> None: """Get withings attributes.""" return self._attribute.__dict__ @@ -378,71 +358,45 @@ class WithingsHealthSensor(Entity): await self._data_manager.update_sleep_summary() await self.async_update_sleep_summary(self._data_manager.sleep_summary) - async def async_update_measure(self, data) -> None: + async def async_update_measure(self, data: MeasureGetMeasResponse) -> None: """Update the measures data.""" - if data is None: - _LOGGER.error("Provided data is None. Setting state to %s", None) - self._state = None - return - measure_type = self._attribute.measure_type _LOGGER.debug( "Finding the unambiguous measure group with measure_type: %s", measure_type ) - measure_groups = [ - g - for g in data - if (not g.is_ambiguous() and g.get_measure(measure_type) is not None) - ] - if not measure_groups: - _LOGGER.debug("No measure groups found, setting state to %s", None) + value = get_measure_value(data, measure_type, MeasureGroupAttribs.UNAMBIGUOUS) + + if value is None: + _LOGGER.debug("Could not find a value, setting state to %s", None) self._state = None return - _LOGGER.debug( - "Sorting list of %s measure groups by date created (DESC)", - len(measure_groups), - ) - measure_groups.sort(key=(lambda g: g.created), reverse=True) + self._state = round(value, 2) - self._state = round(measure_groups[0].get_measure(measure_type), 4) - - async def async_update_sleep_state(self, data) -> None: + async def async_update_sleep_state(self, data: SleepGetResponse) -> None: """Update the sleep state data.""" - if data is None: - _LOGGER.error("Provided data is None. Setting state to %s", None) - self._state = None - return - if not data.series: _LOGGER.debug("No sleep data, setting state to %s", None) self._state = None return - series = sorted(data.series, key=lambda o: o.enddate, reverse=True) + serie = data.series[len(data.series) - 1] + state = None + if serie.state == SleepState.AWAKE: + state = const.STATE_AWAKE + elif serie.state == SleepState.LIGHT: + state = const.STATE_LIGHT + elif serie.state == SleepState.DEEP: + state = const.STATE_DEEP + elif serie.state == SleepState.REM: + state = const.STATE_REM - serie = series[0] + self._state = state - if serie.state == const.MEASURE_TYPE_SLEEP_STATE_AWAKE: - self._state = const.STATE_AWAKE - elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_LIGHT: - self._state = const.STATE_LIGHT - elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_DEEP: - self._state = const.STATE_DEEP - elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_REM: - self._state = const.STATE_REM - else: - self._state = None - - async def async_update_sleep_summary(self, data) -> None: + async def async_update_sleep_summary(self, data: SleepGetSummaryResponse) -> None: """Update the sleep summary data.""" - if data is None: - _LOGGER.error("Provided data is None. Setting state to %s", None) - self._state = None - return - if not data.series: _LOGGER.debug("Sleep data has no series, setting state to %s", None) self._state = None @@ -454,7 +408,59 @@ class WithingsHealthSensor(Entity): _LOGGER.debug("Determining total value for: %s", measurement) total = 0 for serie in data.series: - if hasattr(serie, measure_type): - total += getattr(serie, measure_type) + data = serie.data + value = 0 + if measure_type == GetSleepSummaryField.REM_SLEEP_DURATION.value: + value = data.remsleepduration + elif measure_type == GetSleepSummaryField.WAKEUP_DURATION.value: + value = data.wakeupduration + elif measure_type == GetSleepSummaryField.LIGHT_SLEEP_DURATION.value: + value = data.lightsleepduration + elif measure_type == GetSleepSummaryField.DEEP_SLEEP_DURATION.value: + value = data.deepsleepduration + elif measure_type == GetSleepSummaryField.WAKEUP_COUNT.value: + value = data.wakeupcount + elif measure_type == GetSleepSummaryField.DURATION_TO_SLEEP.value: + value = data.durationtosleep + elif measure_type == GetSleepSummaryField.DURATION_TO_WAKEUP.value: + value = data.durationtowakeup + elif measure_type == GetSleepSummaryField.HR_AVERAGE.value: + value = data.hr_average + elif measure_type == GetSleepSummaryField.HR_MIN.value: + value = data.hr_min + elif measure_type == GetSleepSummaryField.HR_MAX.value: + value = data.hr_max + elif measure_type == GetSleepSummaryField.RR_AVERAGE.value: + value = data.rr_average + elif measure_type == GetSleepSummaryField.RR_MIN.value: + value = data.rr_min + elif measure_type == GetSleepSummaryField.RR_MAX.value: + value = data.rr_max + + # Sometimes a None is provided for value, default to 0. + total += value or 0 self._state = round(total, 4) + + +def create_sensor_entities( + data_manager: WithingsDataManager, user_id: str +) -> List[WithingsHealthSensor]: + """Create sensor entities.""" + entities = [] + + for attribute in WITHINGS_ATTRIBUTES: + _LOGGER.debug( + "Creating entity for measurement: %s, measure_type: %s," + "friendly_name: %s, unit_of_measurement: %s", + attribute.measurement, + attribute.measure_type, + attribute.friendly_name, + attribute.unit_of_measurement, + ) + + entity = WithingsHealthSensor(data_manager, attribute, user_id) + + entities.append(entity) + + return entities diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 1a99abc7255..23be2cd385f 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -2,19 +2,13 @@ "config": { "title": "Withings", "step": { - "user": { + "profile": { "title": "User Profile.", - "description": "Select a user profile to which you want Home Assistant to map with a Withings profile. On the withings page, be sure to select the same user or data will not be labeled correctly.", + "description": "Which profile did you select on the Withings website? It's important the profiles match, otherwise data will be mis-labeled.", "data": { "profile": "Profile" } } - }, - "create_entry": { - "default": "Successfully authenticated with Withings for the selected profile." - }, - "abort": { - "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." } } } diff --git a/requirements_all.txt b/requirements_all.txt index 73cb1e8bf04..4587ed4ed7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1984,7 +1984,7 @@ websockets==6.0 wirelesstagpy==0.4.0 # homeassistant.components.withings -withings-api==2.0.0b8 +withings-api==2.1.2 # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test.txt b/requirements_test.txt index 7af2ec0dde3..5240946b004 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,3 +20,4 @@ pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.2.1 requests_mock==1.7.0 +responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa907170786..a927b5aace3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,6 +21,7 @@ pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.2.1 requests_mock==1.7.0 +responses==0.10.6 # homeassistant.components.homekit @@ -629,7 +630,7 @@ watchdog==0.8.3 websockets==6.0 # homeassistant.components.withings -withings-api==2.0.0b8 +withings-api==2.1.2 # homeassistant.components.bluesound # homeassistant.components.startca diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index f3839a1be55..570d12d79f6 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,213 +1,383 @@ """Common data for for the withings component tests.""" +import re import time +from typing import List -import withings_api as withings +import requests_mock +from withings_api import AbstractWithingsApi +from withings_api.common import ( + MeasureGetMeasGroupAttrib, + MeasureGetMeasGroupCategory, + MeasureType, + SleepModel, + SleepState, +) +from homeassistant import data_entry_flow +import homeassistant.components.api as api +import homeassistant.components.http as http import homeassistant.components.withings.const as const +from homeassistant.config import async_process_ha_core_config +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component +from homeassistant.util import slugify -def new_sleep_data(model, series): - """Create simple dict to simulate api data.""" - return {"series": series, "model": model} +def get_entity_id(measure, profile) -> str: + """Get an entity id for a measure and profile.""" + return "sensor.{}_{}_{}".format(const.DOMAIN, measure, slugify(profile)) -def new_sleep_data_serie(startdate, enddate, state): - """Create simple dict to simulate api data.""" - return {"startdate": startdate, "enddate": enddate, "state": state} +def assert_state_equals( + hass: HomeAssistant, profile: str, measure: str, expected +) -> None: + """Assert the state of a withings sensor.""" + entity_id = get_entity_id(measure, profile) + state_obj = hass.states.get(entity_id) + + assert state_obj, "Expected entity {} to exist but it did not".format(entity_id) + + assert state_obj.state == str( + expected + ), "Expected {} but was {} for measure {}, {}".format( + expected, state_obj.state, measure, entity_id + ) -def new_sleep_summary(timezone, model, startdate, enddate, date, modified, data): - """Create simple dict to simulate api data.""" - return { - "timezone": timezone, - "model": model, - "startdate": startdate, - "enddate": enddate, - "date": date, - "modified": modified, - "data": data, +async def setup_hass(hass: HomeAssistant) -> dict: + """Configure home assistant.""" + profiles = ["Person0", "Person1", "Person2", "Person3", "Person4"] + + hass_config = { + "homeassistant": {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, + api.DOMAIN: {"base_url": "http://localhost/"}, + http.DOMAIN: {"server_port": 8080}, + const.DOMAIN: { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: profiles, + }, } + await async_process_ha_core_config(hass, hass_config.get("homeassistant")) + assert await async_setup_component(hass, http.DOMAIN, hass_config) + assert await async_setup_component(hass, api.DOMAIN, hass_config) + assert await async_setup_component(hass, const.DOMAIN, hass_config) + await hass.async_block_till_done() -def new_sleep_summary_detail( - wakeupduration, - lightsleepduration, - deepsleepduration, - remsleepduration, - wakeupcount, - durationtosleep, - durationtowakeup, - hr_average, - hr_min, - hr_max, - rr_average, - rr_min, - rr_max, -): - """Create simple dict to simulate api data.""" - return { - "wakeupduration": wakeupduration, - "lightsleepduration": lightsleepduration, - "deepsleepduration": deepsleepduration, - "remsleepduration": remsleepduration, - "wakeupcount": wakeupcount, - "durationtosleep": durationtosleep, - "durationtowakeup": durationtowakeup, - "hr_average": hr_average, - "hr_min": hr_min, - "hr_max": hr_max, - "rr_average": rr_average, - "rr_min": rr_min, - "rr_max": rr_max, - } + return hass_config -def new_measure_group( - grpid, attrib, date, created, category, deviceid, more, offset, measures -): - """Create simple dict to simulate api data.""" - return { - "grpid": grpid, - "attrib": attrib, - "date": date, - "created": created, - "category": category, - "deviceid": deviceid, - "measures": measures, - "more": more, - "offset": offset, - "comment": "blah", # deprecated - } +async def configure_integration( + hass: HomeAssistant, + aiohttp_client, + aioclient_mock, + profiles: List[str], + profile_index: int, + get_device_response: dict, + getmeasures_response: dict, + get_sleep_response: dict, + get_sleep_summary_response: dict, +) -> None: + """Configure the integration for a specific profile.""" + selected_profile = profiles[profile_index] - -def new_measure(type_str, value, unit): - """Create simple dict to simulate api data.""" - return { - "value": value, - "type": type_str, - "unit": unit, - "algo": -1, # deprecated - "fm": -1, # deprecated - "fw": -1, # deprecated - } - - -def withings_sleep_response(states): - """Create a sleep response based on states.""" - data = [] - for state in states: - data.append( - new_sleep_data_serie( - "2019-02-01 0{}:00:00".format(str(len(data))), - "2019-02-01 0{}:00:00".format(str(len(data) + 1)), - state, - ) + with requests_mock.mock() as rqmck: + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/v2/user?.*action=getdevice(&.*|$)"), + status_code=200, + json=get_device_response, ) - return withings.WithingsSleep(new_sleep_data("aa", data)) + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/v2/sleep?.*action=get(&.*|$)"), + status_code=200, + json=get_sleep_response, + ) + + rqmck.get( + re.compile( + AbstractWithingsApi.URL + "/v2/sleep?.*action=getsummary(&.*|$)" + ), + status_code=200, + json=get_sleep_summary_response, + ) + + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/measure?.*action=getmeas(&.*|$)"), + status_code=200, + json=getmeasures_response, + ) + + # Get the withings config flow. + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": SOURCE_USER} + ) + assert result + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, {"flow_id": result["flow_id"]} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + "https://account.withings.com/oauth2_user/authorize2?" + "response_type=code&client_id=my_client_id&" + "redirect_uri=http://127.0.0.1:8080/auth/external/callback&" + f"state={state}" + "&scope=user.info,user.metrics,user.activity" + ) + + # Simulate user being redirected from withings site. + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://account.withings.com/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "userid": "myuserid", + }, + ) + + # Present user with a list of profiles to choose from. + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == "form" + assert result.get("step_id") == "profile" + assert result.get("data_schema").schema["profile"].container == profiles + + # Select the user profile. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {const.PROFILE: selected_profile} + ) + + # Finish the config flow by calling it again. + assert result.get("type") == "create_entry" + assert result.get("result") + config_data = result.get("result").data + assert config_data.get(const.PROFILE) == profiles[profile_index] + assert config_data.get("auth_implementation") == const.DOMAIN + assert config_data.get("token") + + # Ensure all the flows are complete. + flows = hass.config_entries.flow.async_progress() + assert not flows + + # Wait for remaining tasks to complete. + await hass.async_block_till_done() -WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( - { - "updatetime": "", - "timezone": "", +WITHINGS_GET_DEVICE_RESPONSE_EMPTY = {"status": 0, "body": {"devices": []}} + + +WITHINGS_GET_DEVICE_RESPONSE = { + "status": 0, + "body": { + "devices": [ + { + "type": "type1", + "model": "model1", + "battery": "battery1", + "deviceid": "deviceid1", + "timezone": "UTC", + } + ] + }, +} + + +WITHINGS_MEASURES_RESPONSE_EMPTY = { + "status": 0, + "body": {"updatetime": "2019-08-01", "timezone": "UTC", "measuregrps": []}, +} + + +WITHINGS_MEASURES_RESPONSE = { + "status": 0, + "body": { + "updatetime": "2019-08-01", + "timezone": "UTC", "measuregrps": [ # Un-ambiguous groups. - new_measure_group( - 1, - 0, - time.time(), - time.time(), - 1, - "DEV_ID", - False, - 0, - [ - new_measure(const.MEASURE_TYPE_WEIGHT, 70, 0), - new_measure(const.MEASURE_TYPE_FAT_MASS, 5, 0), - new_measure(const.MEASURE_TYPE_FAT_MASS_FREE, 60, 0), - new_measure(const.MEASURE_TYPE_MUSCLE_MASS, 50, 0), - new_measure(const.MEASURE_TYPE_BONE_MASS, 10, 0), - new_measure(const.MEASURE_TYPE_HEIGHT, 2, 0), - new_measure(const.MEASURE_TYPE_TEMP, 40, 0), - new_measure(const.MEASURE_TYPE_BODY_TEMP, 35, 0), - new_measure(const.MEASURE_TYPE_SKIN_TEMP, 20, 0), - new_measure(const.MEASURE_TYPE_FAT_RATIO, 70, -3), - new_measure(const.MEASURE_TYPE_DIASTOLIC_BP, 70, 0), - new_measure(const.MEASURE_TYPE_SYSTOLIC_BP, 100, 0), - new_measure(const.MEASURE_TYPE_HEART_PULSE, 60, 0), - new_measure(const.MEASURE_TYPE_SPO2, 95, -2), - new_measure(const.MEASURE_TYPE_HYDRATION, 95, -2), - new_measure(const.MEASURE_TYPE_PWV, 100, 0), + { + "grpid": 1, + "attrib": MeasureGetMeasGroupAttrib.DEVICE_ENTRY_FOR_USER.real, + "date": time.time(), + "created": time.time(), + "category": MeasureGetMeasGroupCategory.REAL.real, + "deviceid": "DEV_ID", + "more": False, + "offset": 0, + "measures": [ + {"type": MeasureType.WEIGHT, "value": 70, "unit": 0}, + {"type": MeasureType.FAT_MASS_WEIGHT, "value": 5, "unit": 0}, + {"type": MeasureType.FAT_FREE_MASS, "value": 60, "unit": 0}, + {"type": MeasureType.MUSCLE_MASS, "value": 50, "unit": 0}, + {"type": MeasureType.BONE_MASS, "value": 10, "unit": 0}, + {"type": MeasureType.HEIGHT, "value": 2, "unit": 0}, + {"type": MeasureType.TEMPERATURE, "value": 40, "unit": 0}, + {"type": MeasureType.BODY_TEMPERATURE, "value": 40, "unit": 0}, + {"type": MeasureType.SKIN_TEMPERATURE, "value": 20, "unit": 0}, + {"type": MeasureType.FAT_RATIO, "value": 70, "unit": -3}, + { + "type": MeasureType.DIASTOLIC_BLOOD_PRESSURE, + "value": 70, + "unit": 0, + }, + { + "type": MeasureType.SYSTOLIC_BLOOD_PRESSURE, + "value": 100, + "unit": 0, + }, + {"type": MeasureType.HEART_RATE, "value": 60, "unit": 0}, + {"type": MeasureType.SP02, "value": 95, "unit": -2}, + {"type": MeasureType.HYDRATION, "value": 95, "unit": -2}, + {"type": MeasureType.PULSE_WAVE_VELOCITY, "value": 100, "unit": 0}, ], - ), + }, # Ambiguous groups (we ignore these) - new_measure_group( - 1, - 1, - time.time(), - time.time(), - 1, - "DEV_ID", - False, - 0, - [ - new_measure(const.MEASURE_TYPE_WEIGHT, 71, 0), - new_measure(const.MEASURE_TYPE_FAT_MASS, 4, 0), - new_measure(const.MEASURE_TYPE_MUSCLE_MASS, 51, 0), - new_measure(const.MEASURE_TYPE_BONE_MASS, 11, 0), - new_measure(const.MEASURE_TYPE_HEIGHT, 201, 0), - new_measure(const.MEASURE_TYPE_TEMP, 41, 0), - new_measure(const.MEASURE_TYPE_BODY_TEMP, 34, 0), - new_measure(const.MEASURE_TYPE_SKIN_TEMP, 21, 0), - new_measure(const.MEASURE_TYPE_FAT_RATIO, 71, -3), - new_measure(const.MEASURE_TYPE_DIASTOLIC_BP, 71, 0), - new_measure(const.MEASURE_TYPE_SYSTOLIC_BP, 101, 0), - new_measure(const.MEASURE_TYPE_HEART_PULSE, 61, 0), - new_measure(const.MEASURE_TYPE_SPO2, 98, -2), - new_measure(const.MEASURE_TYPE_HYDRATION, 96, -2), - new_measure(const.MEASURE_TYPE_PWV, 102, 0), + { + "grpid": 1, + "attrib": MeasureGetMeasGroupAttrib.DEVICE_ENTRY_FOR_USER.real, + "date": time.time(), + "created": time.time(), + "category": MeasureGetMeasGroupCategory.REAL.real, + "deviceid": "DEV_ID", + "more": False, + "offset": 0, + "measures": [ + {"type": MeasureType.WEIGHT, "value": 71, "unit": 0}, + {"type": MeasureType.FAT_MASS_WEIGHT, "value": 4, "unit": 0}, + {"type": MeasureType.FAT_FREE_MASS, "value": 40, "unit": 0}, + {"type": MeasureType.MUSCLE_MASS, "value": 51, "unit": 0}, + {"type": MeasureType.BONE_MASS, "value": 11, "unit": 0}, + {"type": MeasureType.HEIGHT, "value": 201, "unit": 0}, + {"type": MeasureType.TEMPERATURE, "value": 41, "unit": 0}, + {"type": MeasureType.BODY_TEMPERATURE, "value": 34, "unit": 0}, + {"type": MeasureType.SKIN_TEMPERATURE, "value": 21, "unit": 0}, + {"type": MeasureType.FAT_RATIO, "value": 71, "unit": -3}, + { + "type": MeasureType.DIASTOLIC_BLOOD_PRESSURE, + "value": 71, + "unit": 0, + }, + { + "type": MeasureType.SYSTOLIC_BLOOD_PRESSURE, + "value": 101, + "unit": 0, + }, + {"type": MeasureType.HEART_RATE, "value": 61, "unit": 0}, + {"type": MeasureType.SP02, "value": 98, "unit": -2}, + {"type": MeasureType.HYDRATION, "value": 96, "unit": -2}, + {"type": MeasureType.PULSE_WAVE_VELOCITY, "value": 102, "unit": 0}, ], - ), + }, ], - } -) + }, +} -WITHINGS_SLEEP_RESPONSE = withings_sleep_response( - [ - const.MEASURE_TYPE_SLEEP_STATE_AWAKE, - const.MEASURE_TYPE_SLEEP_STATE_LIGHT, - const.MEASURE_TYPE_SLEEP_STATE_REM, - const.MEASURE_TYPE_SLEEP_STATE_DEEP, - ] -) +WITHINGS_SLEEP_RESPONSE_EMPTY = { + "status": 0, + "body": {"model": SleepModel.TRACKER.real, "series": []}, +} -WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( - { + +WITHINGS_SLEEP_RESPONSE = { + "status": 0, + "body": { + "model": SleepModel.TRACKER.real, "series": [ - new_sleep_summary( - "UTC", - 32, - "2019-02-01", - "2019-02-02", - "2019-02-02", - "12345", - new_sleep_summary_detail( - 110, 210, 310, 410, 510, 610, 710, 810, 910, 1010, 1110, 1210, 1310 - ), - ), - new_sleep_summary( - "UTC", - 32, - "2019-02-01", - "2019-02-02", - "2019-02-02", - "12345", - new_sleep_summary_detail( - 210, 310, 410, 510, 610, 710, 810, 910, 1010, 1110, 1210, 1310, 1410 - ), - ), - ] - } -) + { + "startdate": "2019-02-01 00:00:00", + "enddate": "2019-02-01 01:00:00", + "state": SleepState.AWAKE.real, + }, + { + "startdate": "2019-02-01 01:00:00", + "enddate": "2019-02-01 02:00:00", + "state": SleepState.LIGHT.real, + }, + { + "startdate": "2019-02-01 02:00:00", + "enddate": "2019-02-01 03:00:00", + "state": SleepState.REM.real, + }, + { + "startdate": "2019-02-01 03:00:00", + "enddate": "2019-02-01 04:00:00", + "state": SleepState.DEEP.real, + }, + ], + }, +} + + +WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY = { + "status": 0, + "body": {"more": False, "offset": 0, "series": []}, +} + + +WITHINGS_SLEEP_SUMMARY_RESPONSE = { + "status": 0, + "body": { + "more": False, + "offset": 0, + "series": [ + { + "timezone": "UTC", + "model": SleepModel.SLEEP_MONITOR.real, + "startdate": "2019-02-01", + "enddate": "2019-02-02", + "date": "2019-02-02", + "modified": 12345, + "data": { + "wakeupduration": 110, + "lightsleepduration": 210, + "deepsleepduration": 310, + "remsleepduration": 410, + "wakeupcount": 510, + "durationtosleep": 610, + "durationtowakeup": 710, + "hr_average": 810, + "hr_min": 910, + "hr_max": 1010, + "rr_average": 1110, + "rr_min": 1210, + "rr_max": 1310, + }, + }, + { + "timezone": "UTC", + "model": SleepModel.SLEEP_MONITOR.real, + "startdate": "2019-02-01", + "enddate": "2019-02-02", + "date": "2019-02-02", + "modified": 12345, + "data": { + "wakeupduration": 210, + "lightsleepduration": 310, + "deepsleepduration": 410, + "remsleepduration": 510, + "wakeupcount": 610, + "durationtosleep": 710, + "durationtowakeup": 810, + "hr_average": 910, + "hr_min": 1010, + "hr_max": 1110, + "rr_average": 1210, + "rr_min": 1310, + "rr_max": 1410, + }, + }, + ], + }, +} diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py deleted file mode 100644 index 0aa6af0d7c0..00000000000 --- a/tests/components/withings/conftest.py +++ /dev/null @@ -1,350 +0,0 @@ -"""Fixtures for withings tests.""" -import time -from typing import Awaitable, Callable, List - -import asynctest -import withings_api as withings -import pytest - -import homeassistant.components.api as api -import homeassistant.components.http as http -import homeassistant.components.withings.const as const -from homeassistant.components.withings import CONFIG_SCHEMA, DOMAIN -from homeassistant.config import async_process_ha_core_config -from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC -from homeassistant.setup import async_setup_component - -from .common import ( - WITHINGS_MEASURES_RESPONSE, - WITHINGS_SLEEP_RESPONSE, - WITHINGS_SLEEP_SUMMARY_RESPONSE, -) - - -class WithingsFactoryConfig: - """Configuration for withings test fixture.""" - - PROFILE_1 = "Person 1" - PROFILE_2 = "Person 2" - - def __init__( - self, - api_config: dict = None, - http_config: dict = None, - measures: List[str] = None, - unit_system: str = None, - throttle_interval: int = const.THROTTLE_INTERVAL, - withings_request_response="DATA", - withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, - withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, - withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, - ) -> None: - """Constructor.""" - self._throttle_interval = throttle_interval - self._withings_request_response = withings_request_response - self._withings_measures_response = withings_measures_response - self._withings_sleep_response = withings_sleep_response - self._withings_sleep_summary_response = withings_sleep_summary_response - self._withings_config = { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: [ - WithingsFactoryConfig.PROFILE_1, - WithingsFactoryConfig.PROFILE_2, - ], - } - - self._api_config = api_config or {"base_url": "http://localhost/"} - self._http_config = http_config or {} - self._measures = measures - - assert self._withings_config, "withings_config must be set." - assert isinstance( - self._withings_config, dict - ), "withings_config must be a dict." - assert isinstance(self._api_config, dict), "api_config must be a dict." - assert isinstance(self._http_config, dict), "http_config must be a dict." - - self._hass_config = { - "homeassistant": {CONF_UNIT_SYSTEM: unit_system or CONF_UNIT_SYSTEM_METRIC}, - api.DOMAIN: self._api_config, - http.DOMAIN: self._http_config, - DOMAIN: self._withings_config, - } - - @property - def withings_config(self): - """Get withings component config.""" - return self._withings_config - - @property - def api_config(self): - """Get api component config.""" - return self._api_config - - @property - def http_config(self): - """Get http component config.""" - return self._http_config - - @property - def measures(self): - """Get the measures.""" - return self._measures - - @property - def hass_config(self): - """Home assistant config.""" - return self._hass_config - - @property - def throttle_interval(self): - """Throttle interval.""" - return self._throttle_interval - - @property - def withings_request_response(self): - """Request response.""" - return self._withings_request_response - - @property - def withings_measures_response(self) -> withings.WithingsMeasures: - """Measures response.""" - return self._withings_measures_response - - @property - def withings_sleep_response(self) -> withings.WithingsSleep: - """Sleep response.""" - return self._withings_sleep_response - - @property - def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: - """Sleep summary response.""" - return self._withings_sleep_summary_response - - -class WithingsFactoryData: - """Data about the configured withing test component.""" - - def __init__( - self, - hass, - flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, - data_manager_get_throttle_interval_mock, - ): - """Constructor.""" - self._hass = hass - self._flow_id = flow_id - self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock - self._withings_api_request_mock = withings_api_request_mock - self._withings_api_get_measures_mock = withings_api_get_measures_mock - self._withings_api_get_sleep_mock = withings_api_get_sleep_mock - self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock - self._data_manager_get_throttle_interval_mock = ( - data_manager_get_throttle_interval_mock - ) - - @property - def hass(self): - """Get hass instance.""" - return self._hass - - @property - def flow_id(self): - """Get flow id.""" - return self._flow_id - - @property - def withings_auth_get_credentials_mock(self): - """Get auth credentials mock.""" - return self._withings_auth_get_credentials_mock - - @property - def withings_api_request_mock(self): - """Get request mock.""" - return self._withings_api_request_mock - - @property - def withings_api_get_measures_mock(self): - """Get measures mock.""" - return self._withings_api_get_measures_mock - - @property - def withings_api_get_sleep_mock(self): - """Get sleep mock.""" - return self._withings_api_get_sleep_mock - - @property - def withings_api_get_sleep_summary_mock(self): - """Get sleep summary mock.""" - return self._withings_api_get_sleep_summary_mock - - @property - def data_manager_get_throttle_interval_mock(self): - """Get throttle mock.""" - return self._data_manager_get_throttle_interval_mock - - async def configure_user(self): - """Present a form with user profiles.""" - step = await self.hass.config_entries.flow.async_configure(self.flow_id, None) - assert step["step_id"] == "user" - - async def configure_profile(self, profile: str): - """Select the user profile. Present a form with authorization link.""" - print("CONFIG_PROFILE:", profile) - step = await self.hass.config_entries.flow.async_configure( - self.flow_id, {const.PROFILE: profile} - ) - assert step["step_id"] == "auth" - - async def configure_code(self, profile: str, code: str): - """Handle authorization code. Create config entries.""" - step = await self.hass.config_entries.flow.async_configure( - self.flow_id, {const.PROFILE: profile, const.CODE: code} - ) - assert step["type"] == "external_done" - - await self.hass.async_block_till_done() - - step = await self.hass.config_entries.flow.async_configure( - self.flow_id, {const.PROFILE: profile, const.CODE: code} - ) - - assert step["type"] == "create_entry" - - await self.hass.async_block_till_done() - - async def configure_all(self, profile: str, code: str): - """Configure all flow steps.""" - await self.configure_user() - await self.configure_profile(profile) - await self.configure_code(profile, code) - - -WithingsFactory = Callable[[WithingsFactoryConfig], Awaitable[WithingsFactoryData]] - - -@pytest.fixture(name="withings_factory") -def withings_factory_fixture(request, hass) -> WithingsFactory: - """Home assistant platform fixture.""" - patches = [] - - async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: - CONFIG_SCHEMA(config.hass_config.get(DOMAIN)) - - await async_process_ha_core_config( - hass, config.hass_config.get("homeassistant") - ) - assert await async_setup_component(hass, http.DOMAIN, config.hass_config) - assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - - withings_auth_get_credentials_patch = asynctest.patch( - "withings_api.WithingsAuth.get_credentials", - return_value=withings.WithingsCredentials( - access_token="my_access_token", - token_expiry=time.time() + 600, - token_type="my_token_type", - refresh_token="my_refresh_token", - user_id="my_user_id", - client_id=config.withings_config.get(const.CLIENT_ID), - consumer_secret=config.withings_config.get(const.CLIENT_SECRET), - ), - ) - withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - - withings_api_request_patch = asynctest.patch( - "withings_api.WithingsApi.request", - return_value=config.withings_request_response, - ) - withings_api_request_mock = withings_api_request_patch.start() - - withings_api_get_measures_patch = asynctest.patch( - "withings_api.WithingsApi.get_measures", - return_value=config.withings_measures_response, - ) - withings_api_get_measures_mock = withings_api_get_measures_patch.start() - - withings_api_get_sleep_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep", - return_value=config.withings_sleep_response, - ) - withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - - withings_api_get_sleep_summary_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep_summary", - return_value=config.withings_sleep_summary_response, - ) - withings_api_get_sleep_summary_mock = ( - withings_api_get_sleep_summary_patch.start() - ) - - data_manager_get_throttle_interval_patch = asynctest.patch( - "homeassistant.components.withings.common.WithingsDataManager" - ".get_throttle_interval", - return_value=config.throttle_interval, - ) - data_manager_get_throttle_interval_mock = ( - data_manager_get_throttle_interval_patch.start() - ) - - get_measures_patch = asynctest.patch( - "homeassistant.components.withings.sensor.get_measures", - return_value=config.measures, - ) - get_measures_patch.start() - - patches.extend( - [ - withings_auth_get_credentials_patch, - withings_api_request_patch, - withings_api_get_measures_patch, - withings_api_get_sleep_patch, - withings_api_get_sleep_summary_patch, - data_manager_get_throttle_interval_patch, - get_measures_patch, - ] - ) - - # Collect the flow id. - tasks = [] - - orig_async_create_task = hass.async_create_task - - def create_task(*args): - task = orig_async_create_task(*args) - tasks.append(task) - return task - - async_create_task_patch = asynctest.patch.object( - hass, "async_create_task", side_effect=create_task - ) - - with async_create_task_patch: - assert await async_setup_component(hass, DOMAIN, config.hass_config) - await hass.async_block_till_done() - - flow_id = tasks[2].result()["flow_id"] - - return WithingsFactoryData( - hass, - flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, - data_manager_get_throttle_interval_mock, - ) - - def cleanup(): - for patch in patches: - patch.stop() - - request.addfinalizer(cleanup) - - return factory diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 9f2480f9094..e513ebb1d2e 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,34 +1,33 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import withings_api as withings -from oauthlib.oauth2.rfc6749.errors import MissingTokenError -import pytest -from requests_oauthlib import TokenUpdated +import pytest +from withings_api import WithingsApi +from withings_api.common import UnauthorizedException, TimeoutException + +from homeassistant.exceptions import PlatformNotReady from homeassistant.components.withings.common import ( NotAuthenticatedError, - ServiceError, WithingsDataManager, ) -from homeassistant.exceptions import PlatformNotReady @pytest.fixture(name="withings_api") -def withings_api_fixture(): +def withings_api_fixture() -> WithingsApi: """Provide withings api.""" - withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api = WithingsApi.__new__(WithingsApi) withings_api.get_measures = MagicMock() withings_api.get_sleep = MagicMock() return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, withings_api: withings.WithingsApi): +def data_manager_fixture(hass, withings_api: WithingsApi) -> WithingsDataManager: """Provide data manager.""" return WithingsDataManager(hass, "My Profile", withings_api) -def test_print_service(): +def test_print_service() -> None: """Test method.""" # Go from None to True WithingsDataManager.service_available = None @@ -57,54 +56,27 @@ def test_print_service(): assert not WithingsDataManager.print_service_unavailable() -async def test_data_manager_call(data_manager): +async def test_data_manager_call(data_manager: WithingsDataManager) -> None: """Test method.""" - # Token refreshed. - def hello_func(): - return "HELLO2" - - function = MagicMock(side_effect=[TokenUpdated("my_token"), hello_func()]) - result = await data_manager.call(function) - assert result == "HELLO2" - assert function.call_count == 2 - - # Too many token refreshes. - function = MagicMock( - side_effect=[TokenUpdated("my_token"), TokenUpdated("my_token")] - ) - try: - result = await data_manager.call(function) - assert False, "This should not have ran." - except ServiceError: - assert True - assert function.call_count == 2 - # Not authenticated 1. - test_function = MagicMock(side_effect=MissingTokenError("Error Code 401")) - try: - result = await data_manager.call(test_function) - assert False, "An exception should have been thrown." - except NotAuthenticatedError: - assert True + test_function = MagicMock(side_effect=UnauthorizedException(401)) + with pytest.raises(NotAuthenticatedError): + await data_manager.call(test_function) # Not authenticated 2. - test_function = MagicMock(side_effect=Exception("Error Code 401")) - try: - result = await data_manager.call(test_function) - assert False, "An exception should have been thrown." - except NotAuthenticatedError: - assert True + test_function = MagicMock(side_effect=TimeoutException(522)) + with pytest.raises(PlatformNotReady): + await data_manager.call(test_function) # Service error. test_function = MagicMock(side_effect=PlatformNotReady()) - try: - result = await data_manager.call(test_function) - assert False, "An exception should have been thrown." - except PlatformNotReady: - assert True + with pytest.raises(PlatformNotReady): + await data_manager.call(test_function) -async def test_data_manager_call_throttle_enabled(data_manager): +async def test_data_manager_call_throttle_enabled( + data_manager: WithingsDataManager +) -> None: """Test method.""" hello_func = MagicMock(return_value="HELLO2") @@ -117,7 +89,9 @@ async def test_data_manager_call_throttle_enabled(data_manager): assert hello_func.call_count == 1 -async def test_data_manager_call_throttle_disabled(data_manager): +async def test_data_manager_call_throttle_disabled( + data_manager: WithingsDataManager +) -> None: """Test method.""" hello_func = MagicMock(return_value="HELLO2") diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py deleted file mode 100644 index 3ae9d11c3b6..00000000000 --- a/tests/components/withings/test_config_flow.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Tests for the Withings config flow.""" -from aiohttp.web_request import BaseRequest -from asynctest import CoroutineMock, MagicMock -import pytest - -from homeassistant import data_entry_flow -from homeassistant.components.withings import const -from homeassistant.components.withings.config_flow import ( - register_flow_implementation, - WithingsFlowHandler, - WithingsAuthCallbackView, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType - - -@pytest.fixture(name="flow_handler") -def flow_handler_fixture(hass: HomeAssistantType): - """Provide flow handler.""" - flow_handler = WithingsFlowHandler() - flow_handler.hass = hass - return flow_handler - - -def test_flow_handler_init(flow_handler: WithingsFlowHandler): - """Test the init of the flow handler.""" - assert not flow_handler.flow_profile - - -def test_flow_handler_async_profile_config_entry( - hass: HomeAssistantType, flow_handler: WithingsFlowHandler -): - """Test profile config entry.""" - config_entries = [ - ConfigEntry( - version=1, - domain=const.DOMAIN, - title="AAA", - data={}, - source="source", - connection_class="connection_class", - system_options={}, - ), - ConfigEntry( - version=1, - domain=const.DOMAIN, - title="Person 1", - data={const.PROFILE: "Person 1"}, - source="source", - connection_class="connection_class", - system_options={}, - ), - ConfigEntry( - version=1, - domain=const.DOMAIN, - title="BBB", - data={}, - source="source", - connection_class="connection_class", - system_options={}, - ), - ] - - hass.config_entries.async_entries = MagicMock(return_value=config_entries) - - config_entry = flow_handler.async_profile_config_entry - - assert not config_entry("GGGG") - hass.config_entries.async_entries.assert_called_with(const.DOMAIN) - - assert not config_entry("CCC") - hass.config_entries.async_entries.assert_called_with(const.DOMAIN) - - assert config_entry("Person 1") == config_entries[1] - hass.config_entries.async_entries.assert_called_with(const.DOMAIN) - - -def test_flow_handler_get_auth_client( - hass: HomeAssistantType, flow_handler: WithingsFlowHandler -): - """Test creation of an auth client.""" - register_flow_implementation( - hass, "my_client_id", "my_client_secret", "http://localhost/", ["Person 1"] - ) - - client = flow_handler.get_auth_client("Person 1") - assert client.client_id == "my_client_id" - assert client.consumer_secret == "my_client_secret" - assert client.callback_uri.startswith( - "http://localhost/api/withings/authorize?flow_id=" - ) - assert client.callback_uri.endswith("&profile=Person 1") - assert client.scope == "user.info,user.metrics,user.activity" - - -async def test_auth_callback_view_get(hass: HomeAssistantType): - """Test get api path.""" - view = WithingsAuthCallbackView() - hass.config_entries.flow.async_configure = CoroutineMock(return_value="AAAA") - - request = MagicMock(spec=BaseRequest) - request.app = {"hass": hass} - - # No args - request.query = {} - response = await view.get(request) - assert response.status == 400 - hass.config_entries.flow.async_configure.assert_not_called() - hass.config_entries.flow.async_configure.reset_mock() - - # Checking flow_id - request.query = {"flow_id": "my_flow_id"} - response = await view.get(request) - assert response.status == 400 - hass.config_entries.flow.async_configure.assert_not_called() - hass.config_entries.flow.async_configure.reset_mock() - - # Checking flow_id and profile - request.query = {"flow_id": "my_flow_id", "profile": "my_profile"} - response = await view.get(request) - assert response.status == 400 - hass.config_entries.flow.async_configure.assert_not_called() - hass.config_entries.flow.async_configure.reset_mock() - - # Checking flow_id, profile, code - request.query = { - "flow_id": "my_flow_id", - "profile": "my_profile", - "code": "my_code", - } - response = await view.get(request) - assert response.status == 200 - hass.config_entries.flow.async_configure.assert_called_with( - "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} - ) - hass.config_entries.flow.async_configure.reset_mock() - - # Exception thrown - hass.config_entries.flow.async_configure = CoroutineMock( - side_effect=data_entry_flow.UnknownFlow() - ) - request.query = { - "flow_id": "my_flow_id", - "profile": "my_profile", - "code": "my_code", - } - response = await view.get(request) - assert response.status == 400 - hass.config_entries.flow.async_configure.assert_called_with( - "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} - ) - hass.config_entries.flow.async_configure.reset_mock() - - -async def test_init_without_config(hass): - """Try initializin a configg flow without it being configured.""" - result = await hass.config_entries.flow.async_init( - "withings", context={"source": "user"} - ) - - assert result["type"] == "abort" - assert result["reason"] == "no_flows" diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 609fc1678ea..bd4940d9504 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -1,29 +1,46 @@ """Tests for the Withings component.""" +import re +import time + from asynctest import MagicMock +import requests_mock import voluptuous as vol +from withings_api import AbstractWithingsApi +from withings_api.common import SleepModel, SleepState -import homeassistant.components.api as api import homeassistant.components.http as http -from homeassistant.components.withings import async_setup, const, CONFIG_SCHEMA +from homeassistant.components.withings import ( + async_setup, + async_setup_entry, + const, + CONFIG_SCHEMA, +) +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant -from .conftest import WithingsFactory, WithingsFactoryConfig - -BASE_HASS_CONFIG = { - http.DOMAIN: {}, - api.DOMAIN: {"base_url": "http://localhost/"}, - const.DOMAIN: None, -} +from .common import ( + assert_state_equals, + configure_integration, + setup_hass, + WITHINGS_GET_DEVICE_RESPONSE, + WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_RESPONSE_EMPTY, + WITHINGS_SLEEP_SUMMARY_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_MEASURES_RESPONSE_EMPTY, +) -def config_schema_validate(withings_config): +def config_schema_validate(withings_config) -> None: """Assert a schema config succeeds.""" - hass_config = BASE_HASS_CONFIG.copy() - hass_config[const.DOMAIN] = withings_config + hass_config = {http.DOMAIN: {}, const.DOMAIN: withings_config} return CONFIG_SCHEMA(hass_config) -def config_schema_assert_fail(withings_config): +def config_schema_assert_fail(withings_config) -> None: """Assert a schema config will fail.""" try: config_schema_validate(withings_config) @@ -32,7 +49,7 @@ def config_schema_assert_fail(withings_config): assert True -def test_config_schema_basic_config(): +def test_config_schema_basic_config() -> None: """Test schema.""" config_schema_validate( { @@ -43,7 +60,7 @@ def test_config_schema_basic_config(): ) -def test_config_schema_client_id(): +def test_config_schema_client_id() -> None: """Test schema.""" config_schema_assert_fail( { @@ -67,7 +84,7 @@ def test_config_schema_client_id(): ) -def test_config_schema_client_secret(): +def test_config_schema_client_secret() -> None: """Test schema.""" config_schema_assert_fail( {const.CLIENT_ID: "my_client_id", const.PROFILES: ["Person 1"]} @@ -88,7 +105,7 @@ def test_config_schema_client_secret(): ) -def test_config_schema_profiles(): +def test_config_schema_profiles() -> None: """Test schema.""" config_schema_assert_fail( {const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret"} @@ -130,50 +147,7 @@ def test_config_schema_profiles(): ) -def test_config_schema_base_url(): - """Test schema.""" - config_schema_validate( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: ["Person 1"], - } - ) - config_schema_assert_fail( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.BASE_URL: 123, - const.PROFILES: ["Person 1"], - } - ) - config_schema_assert_fail( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.BASE_URL: "", - const.PROFILES: ["Person 1"], - } - ) - config_schema_assert_fail( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.BASE_URL: "blah blah", - const.PROFILES: ["Person 1"], - } - ) - config_schema_validate( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.BASE_URL: "https://www.blah.blah.blah/blah/blah", - const.PROFILES: ["Person 1"], - } - ) - - -async def test_async_setup_no_config(hass): +async def test_async_setup_no_config(hass: HomeAssistant) -> None: """Test method.""" hass.async_create_task = MagicMock() @@ -182,15 +156,258 @@ async def test_async_setup_no_config(hass): hass.async_create_task.assert_not_called() -async def test_async_setup_teardown(withings_factory: WithingsFactory, hass): - """Test method.""" - data = await withings_factory(WithingsFactoryConfig(measures=[const.MEAS_TEMP_C])) +async def test_upgrade_token( + hass: HomeAssistant, aiohttp_client, aioclient_mock +) -> None: + """Test upgrading from old config data format to new one.""" + config = await setup_hass(hass) + profiles = config[const.DOMAIN][const.PROFILES] - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=0, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response=WITHINGS_SLEEP_RESPONSE_EMPTY, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) entries = hass.config_entries.async_entries(const.DOMAIN) assert entries + entry = entries[0] + data = entry.data + token = data.get("token") + hass.config_entries.async_update_entry( + entry, + data={ + const.PROFILE: data.get(const.PROFILE), + const.CREDENTIALS: { + "access_token": token.get("access_token"), + "refresh_token": token.get("refresh_token"), + "token_expiry": token.get("expires_at"), + "token_type": token.get("type"), + "userid": token.get("userid"), + "client_id": token.get("my_client_id"), + "consumer_secret": token.get("my_consumer_secret"), + }, + }, + ) + + with requests_mock.mock() as rqmck: + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/v2/user?.*action=getdevice(&.*|$)"), + status_code=200, + json=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + ) + + assert await async_setup_entry(hass, entry) + + entries = hass.config_entries.async_entries(const.DOMAIN) + assert entries + + data = entries[0].data + + assert data.get("auth_implementation") == const.DOMAIN + assert data.get("implementation") == const.DOMAIN + assert data.get(const.PROFILE) == profiles[0] + + token = data.get("token") + assert token + assert token.get("access_token") == "mock-access-token" + assert token.get("refresh_token") == "mock-refresh-token" + assert token.get("expires_at") > time.time() + assert token.get("type") == "Bearer" + assert token.get("userid") == "myuserid" + assert not token.get("client_id") + assert not token.get("consumer_secret") + + +async def test_auth_failure( + hass: HomeAssistant, aiohttp_client, aioclient_mock +) -> None: + """Test auth failure.""" + config = await setup_hass(hass) + profiles = config[const.DOMAIN][const.PROFILES] + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=0, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response=WITHINGS_SLEEP_RESPONSE_EMPTY, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) + + entries = hass.config_entries.async_entries(const.DOMAIN) + assert entries + + entry = entries[0] + hass.config_entries.async_update_entry( + entry, data={**entry.data, **{"new_item": 1}} + ) + + with requests_mock.mock() as rqmck: + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/v2/user?.*action=getdevice(&.*|$)"), + status_code=200, + json={"status": 401, "body": {}}, + ) + + assert not (await async_setup_entry(hass, entry)) + + +async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) -> None: + """Test the whole component lifecycle.""" + config = await setup_hass(hass) + profiles = config[const.DOMAIN][const.PROFILES] + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=0, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE, + getmeasures_response=WITHINGS_MEASURES_RESPONSE, + get_sleep_response=WITHINGS_SLEEP_RESPONSE, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE, + ) + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=1, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response=WITHINGS_SLEEP_RESPONSE_EMPTY, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=2, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response={ + "status": 0, + "body": { + "model": SleepModel.TRACKER.real, + "series": [ + { + "startdate": "2019-02-01 00:00:00", + "enddate": "2019-02-01 01:00:00", + "state": SleepState.AWAKE.real, + } + ], + }, + }, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=3, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response={ + "status": 0, + "body": { + "model": SleepModel.TRACKER.real, + "series": [ + { + "startdate": "2019-02-01 00:00:00", + "enddate": "2019-02-01 01:00:00", + "state": SleepState.LIGHT.real, + } + ], + }, + }, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=4, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response={ + "status": 0, + "body": { + "model": SleepModel.TRACKER.real, + "series": [ + { + "startdate": "2019-02-01 00:00:00", + "enddate": "2019-02-01 01:00:00", + "state": SleepState.REM.real, + } + ], + }, + }, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) + + # Test the states of the entities. + expected_states = ( + (profiles[0], const.MEAS_WEIGHT_KG, 70.0), + (profiles[0], const.MEAS_FAT_MASS_KG, 5.0), + (profiles[0], const.MEAS_FAT_FREE_MASS_KG, 60.0), + (profiles[0], const.MEAS_MUSCLE_MASS_KG, 50.0), + (profiles[0], const.MEAS_BONE_MASS_KG, 10.0), + (profiles[0], const.MEAS_HEIGHT_M, 2.0), + (profiles[0], const.MEAS_FAT_RATIO_PCT, 0.07), + (profiles[0], const.MEAS_DIASTOLIC_MMHG, 70.0), + (profiles[0], const.MEAS_SYSTOLIC_MMGH, 100.0), + (profiles[0], const.MEAS_HEART_PULSE_BPM, 60.0), + (profiles[0], const.MEAS_SPO2_PCT, 0.95), + (profiles[0], const.MEAS_HYDRATION, 0.95), + (profiles[0], const.MEAS_PWV, 100.0), + (profiles[0], const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, 320), + (profiles[0], const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, 520), + (profiles[0], const.MEAS_SLEEP_DEEP_DURATION_SECONDS, 720), + (profiles[0], const.MEAS_SLEEP_REM_DURATION_SECONDS, 920), + (profiles[0], const.MEAS_SLEEP_WAKEUP_COUNT, 1120), + (profiles[0], const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, 1320), + (profiles[0], const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, 1520), + (profiles[0], const.MEAS_SLEEP_HEART_RATE_AVERAGE, 1720), + (profiles[0], const.MEAS_SLEEP_HEART_RATE_MIN, 1920), + (profiles[0], const.MEAS_SLEEP_HEART_RATE_MAX, 2120), + (profiles[0], const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, 2320), + (profiles[0], const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, 2520), + (profiles[0], const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, 2720), + (profiles[0], const.MEAS_SLEEP_STATE, const.STATE_DEEP), + (profiles[1], const.MEAS_SLEEP_STATE, STATE_UNKNOWN), + (profiles[1], const.MEAS_HYDRATION, STATE_UNKNOWN), + (profiles[2], const.MEAS_SLEEP_STATE, const.STATE_AWAKE), + (profiles[3], const.MEAS_SLEEP_STATE, const.STATE_LIGHT), + (profiles[3], const.MEAS_FAT_FREE_MASS_KG, STATE_UNKNOWN), + (profiles[4], const.MEAS_SLEEP_STATE, const.STATE_REM), + ) + for (profile, meas, value) in expected_states: + assert_state_equals(hass, profile, meas, value) + + # Tear down setup entries. + entries = hass.config_entries.async_entries(const.DOMAIN) + assert entries + for entry in entries: await hass.config_entries.async_unload(entry.entry_id) + + await hass.async_block_till_done() diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py deleted file mode 100644 index 697d0a8b864..00000000000 --- a/tests/components/withings/test_sensor.py +++ /dev/null @@ -1,310 +0,0 @@ -"""Tests for the Withings component.""" -from unittest.mock import MagicMock, patch - -import asynctest -from withings_api import ( - WithingsApi, - WithingsMeasures, - WithingsSleep, - WithingsSleepSummary, -) -import pytest - -from homeassistant.components.withings import DOMAIN -from homeassistant.components.withings.common import NotAuthenticatedError -import homeassistant.components.withings.const as const -from homeassistant.components.withings.sensor import async_setup_entry -from homeassistant.config_entries import ConfigEntry, SOURCE_USER -from homeassistant.const import STATE_UNKNOWN -from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util import slugify - -from .common import withings_sleep_response -from .conftest import WithingsFactory, WithingsFactoryConfig - - -def get_entity_id(measure, profile): - """Get an entity id for a measure and profile.""" - return "sensor.{}_{}_{}".format(DOMAIN, measure, slugify(profile)) - - -def assert_state_equals(hass: HomeAssistantType, profile: str, measure: str, expected): - """Assert the state of a withings sensor.""" - entity_id = get_entity_id(measure, profile) - state_obj = hass.states.get(entity_id) - - assert state_obj, "Expected entity {} to exist but it did not".format(entity_id) - - assert state_obj.state == str( - expected - ), "Expected {} but was {} for measure {}".format( - expected, state_obj.state, measure - ) - - -async def test_health_sensor_properties(withings_factory: WithingsFactory): - """Test method.""" - data = await withings_factory(WithingsFactoryConfig(measures=[const.MEAS_HEIGHT_M])) - - await data.configure_all(WithingsFactoryConfig.PROFILE_1, "authorization_code") - - state = data.hass.states.get("sensor.withings_height_m_person_1") - state_dict = state.as_dict() - assert state_dict.get("state") == "2" - assert state_dict.get("attributes") == { - "measurement": "height_m", - "measure_type": 4, - "friendly_name": "Withings height_m person_1", - "unit_of_measurement": "m", - "icon": "mdi:ruler", - } - - -SENSOR_TEST_DATA = [ - (const.MEAS_WEIGHT_KG, 70), - (const.MEAS_FAT_MASS_KG, 5), - (const.MEAS_FAT_FREE_MASS_KG, 60), - (const.MEAS_MUSCLE_MASS_KG, 50), - (const.MEAS_BONE_MASS_KG, 10), - (const.MEAS_HEIGHT_M, 2), - (const.MEAS_FAT_RATIO_PCT, 0.07), - (const.MEAS_DIASTOLIC_MMHG, 70), - (const.MEAS_SYSTOLIC_MMGH, 100), - (const.MEAS_HEART_PULSE_BPM, 60), - (const.MEAS_SPO2_PCT, 0.95), - (const.MEAS_HYDRATION, 0.95), - (const.MEAS_PWV, 100), - (const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, 320), - (const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, 520), - (const.MEAS_SLEEP_DEEP_DURATION_SECONDS, 720), - (const.MEAS_SLEEP_REM_DURATION_SECONDS, 920), - (const.MEAS_SLEEP_WAKEUP_COUNT, 1120), - (const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, 1320), - (const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, 1520), - (const.MEAS_SLEEP_HEART_RATE_AVERAGE, 1720), - (const.MEAS_SLEEP_HEART_RATE_MIN, 1920), - (const.MEAS_SLEEP_HEART_RATE_MAX, 2120), - (const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, 2320), - (const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, 2520), - (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, 2720), -] - - -@pytest.mark.parametrize("measure,expected", SENSOR_TEST_DATA) -async def test_health_sensor_throttled( - withings_factory: WithingsFactory, measure, expected -): - """Test method.""" - data = await withings_factory(WithingsFactoryConfig(measures=measure)) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - # Checking initial data. - assert_state_equals(data.hass, profile, measure, expected) - - # Encountering a throttled data. - await async_update_entity(data.hass, get_entity_id(measure, profile)) - - assert_state_equals(data.hass, profile, measure, expected) - - -NONE_SENSOR_TEST_DATA = [ - (const.MEAS_WEIGHT_KG, STATE_UNKNOWN), - (const.MEAS_SLEEP_STATE, STATE_UNKNOWN), - (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, STATE_UNKNOWN), -] - - -@pytest.mark.parametrize("measure,expected", NONE_SENSOR_TEST_DATA) -async def test_health_sensor_state_none( - withings_factory: WithingsFactory, measure, expected -): - """Test method.""" - data = await withings_factory( - WithingsFactoryConfig( - measures=measure, - withings_measures_response=None, - withings_sleep_response=None, - withings_sleep_summary_response=None, - ) - ) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - # Checking initial data. - assert_state_equals(data.hass, profile, measure, expected) - - # Encountering a throttled data. - await async_update_entity(data.hass, get_entity_id(measure, profile)) - - assert_state_equals(data.hass, profile, measure, expected) - - -EMPTY_SENSOR_TEST_DATA = [ - (const.MEAS_WEIGHT_KG, STATE_UNKNOWN), - (const.MEAS_SLEEP_STATE, STATE_UNKNOWN), - (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, STATE_UNKNOWN), -] - - -@pytest.mark.parametrize("measure,expected", EMPTY_SENSOR_TEST_DATA) -async def test_health_sensor_state_empty( - withings_factory: WithingsFactory, measure, expected -): - """Test method.""" - data = await withings_factory( - WithingsFactoryConfig( - measures=measure, - withings_measures_response=WithingsMeasures({"measuregrps": []}), - withings_sleep_response=WithingsSleep({"series": []}), - withings_sleep_summary_response=WithingsSleepSummary({"series": []}), - ) - ) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - # Checking initial data. - assert_state_equals(data.hass, profile, measure, expected) - - # Encountering a throttled data. - await async_update_entity(data.hass, get_entity_id(measure, profile)) - - assert_state_equals(data.hass, profile, measure, expected) - - -SLEEP_STATES_TEST_DATA = [ - ( - const.STATE_AWAKE, - [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_AWAKE], - ), - ( - const.STATE_LIGHT, - [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_LIGHT], - ), - ( - const.STATE_REM, - [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_REM], - ), - ( - const.STATE_DEEP, - [const.MEASURE_TYPE_SLEEP_STATE_LIGHT, const.MEASURE_TYPE_SLEEP_STATE_DEEP], - ), - (const.STATE_UNKNOWN, [const.MEASURE_TYPE_SLEEP_STATE_LIGHT, "blah,"]), -] - - -@pytest.mark.parametrize("expected,sleep_states", SLEEP_STATES_TEST_DATA) -async def test_sleep_state_throttled( - withings_factory: WithingsFactory, expected, sleep_states -): - """Test method.""" - measure = const.MEAS_SLEEP_STATE - - data = await withings_factory( - WithingsFactoryConfig( - measures=[measure], - withings_sleep_response=withings_sleep_response(sleep_states), - ) - ) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - # Check initial data. - assert_state_equals(data.hass, profile, measure, expected) - - # Encountering a throttled data. - await async_update_entity(data.hass, get_entity_id(measure, profile)) - - assert_state_equals(data.hass, profile, measure, expected) - - -async def test_async_setup_check_credentials( - hass: HomeAssistantType, withings_factory: WithingsFactory -): - """Test method.""" - check_creds_patch = asynctest.patch( - "homeassistant.components.withings.common.WithingsDataManager" - ".check_authenticated", - side_effect=NotAuthenticatedError(), - ) - - async_init_patch = asynctest.patch.object( - hass.config_entries.flow, - "async_init", - wraps=hass.config_entries.flow.async_init, - ) - - with check_creds_patch, async_init_patch as async_init_mock: - data = await withings_factory( - WithingsFactoryConfig(measures=[const.MEAS_HEIGHT_M]) - ) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - async_init_mock.assert_called_with( - const.DOMAIN, - context={"source": SOURCE_USER, const.PROFILE: profile}, - data={}, - ) - - -async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): - """Test method.""" - expected_creds = { - "access_token": "my_access_token2", - "refresh_token": "my_refresh_token2", - "token_type": "my_token_type2", - "expires_in": "2", - } - - original_withings_api = WithingsApi - withings_api_instance = None - - def new_withings_api(*args, **kwargs): - nonlocal withings_api_instance - withings_api_instance = original_withings_api(*args, **kwargs) - withings_api_instance.request = MagicMock() - return withings_api_instance - - withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) - session_patch = patch("requests_oauthlib.OAuth2Session") - client_patch = patch("oauthlib.oauth2.WebApplicationClient") - update_entry_patch = patch.object( - hass.config_entries, - "async_update_entry", - wraps=hass.config_entries.async_update_entry, - ) - - with session_patch, client_patch, withings_api_patch, update_entry_patch: - async_add_entities = MagicMock() - hass.config_entries.async_update_entry = MagicMock() - config_entry = ConfigEntry( - version=1, - domain=const.DOMAIN, - title="my title", - data={ - const.PROFILE: "Person 1", - const.CREDENTIALS: { - "access_token": "my_access_token", - "refresh_token": "my_refresh_token", - "token_type": "my_token_type", - "token_expiry": "9999999999", - }, - }, - source="source", - connection_class="conn_class", - system_options={}, - ) - - await async_setup_entry(hass, config_entry, async_add_entities) - - withings_api_instance.set_token(expected_creds) - - new_creds = config_entry.data[const.CREDENTIALS] - assert new_creds["access_token"] == "my_access_token2" From 8791a48328076dd61c1f3e80c10dc54a7c8c9c18 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 20:24:46 +0200 Subject: [PATCH 011/306] Bump aioesphomeapi to 2.4.1 (#28170) * Bump aioesphomeapi to 2.4.1 * Update requirements * Bump to 2.4.2 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b2286b8ab67..40691c653f5 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.4.0" + "aioesphomeapi==2.4.2" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 4587ed4ed7b..069253d9d23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.4.0 +aioesphomeapi==2.4.2 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a927b5aace3..eb38ac61e2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -71,7 +71,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.4.0 +aioesphomeapi==2.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http From b61218f90ebccfc5c5258218c09d1705c0cbc755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 24 Oct 2019 21:31:58 +0200 Subject: [PATCH 012/306] Tradfri config flow enhancements (#28179) --- homeassistant/components/tradfri/__init__.py | 5 +++-- homeassistant/components/tradfri/config_flow.py | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index bdfabb4b00a..9d1a43b240f 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -8,6 +8,7 @@ from pytradfri.api.aiocoap_api import APIFactory import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.util.json import load_json from . import config_flow # noqa pylint_disable=unused-import from .const import ( @@ -113,8 +114,8 @@ async def async_setup_entry(hass, entry): try: gateway_info = await api(gateway.get_gateway_info()) except RequestError: - _LOGGER.error("Tradfri setup failed.") - return False + await factory.shutdown() + raise ConfigEntryNotReady hass.data.setdefault(KEY_API, {})[entry.entry_id] = api hass.data.setdefault(KEY_GATEWAY, {})[entry.entry_id] = gateway diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index bdb195cf53f..24c3fbc1876 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -64,13 +64,17 @@ class FlowHandler(config_entries.ConfigFlow): errors[KEY_SECURITY_CODE] = err.code else: errors["base"] = err.code + else: + user_input = {} fields = OrderedDict() if self._host is None: - fields[vol.Required(CONF_HOST)] = str + fields[vol.Required(CONF_HOST, default=user_input.get(CONF_HOST))] = str - fields[vol.Required(KEY_SECURITY_CODE)] = str + fields[ + vol.Required(KEY_SECURITY_CODE, default=user_input.get(KEY_SECURITY_CODE)) + ] = str return self.async_show_form( step_id="auth", data_schema=vol.Schema(fields), errors=errors From 322d8c2dd55b8927ea9fe8118bf22bf817bf5bbf Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 22:36:47 +0200 Subject: [PATCH 013/306] Fix ESPHome stacktraces when removing entity and shutting down (#28185) --- homeassistant/components/esphome/__init__.py | 20 +++++++++++++++++-- .../components/esphome/entry_data.py | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index dd4ac699089..a669726ca38 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -95,8 +95,11 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener .onetime_listener>" entry_data.cleanup_callbacks.append( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) @callback @@ -365,6 +368,7 @@ async def platform_async_setup_entry( """ entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] entry_data.info[component_key] = {} + entry_data.old_info[component_key] = {} entry_data.state[component_key] = {} @callback @@ -390,7 +394,13 @@ async def platform_async_setup_entry( # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) + + # First copy the now-old info into the backup object + entry_data.old_info[component_key] = entry_data.info[component_key] + # Then update the actual info entry_data.info[component_key] = new_infos + + # Add entities to Home Assistant async_add_entities(add_entities) signal = DISPATCHER_ON_LIST.format(entry_id=entry.entry_id) @@ -524,7 +534,13 @@ class EsphomeEntity(Entity): @property def _static_info(self) -> EntityInfo: - return self._entry_data.info[self._component_key][self._key] + # Check if value is in info database. Use a single lookup. + info = self._entry_data.info[self._component_key].get(self._key) + if info is not None: + return info + # This entity is in the removal project and has been removed from .info + # already, look in old_info + return self._entry_data.old_info[self._component_key].get(self._key) @property def _device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index b7f9ad9b347..d916e1a90c8 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -56,6 +56,13 @@ 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) + + # A second list of EntityInfo objects + # This is necessary for when an entity is being removed. HA requires + # some static info to be accessible during removal (unique_id, maybe others) + # If an entity can't find anything in the info array, it will look for info here. + old_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) From ec478ab8487f0ccd6ac9e1c488f0d217087adc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 24 Oct 2019 22:38:24 +0200 Subject: [PATCH 014/306] Add stop feature to tradfri covers (#28180) * Tradfri cover enhancements * tradfri dependency update * Revert addition of battery attrubite * Remove the supported_features property * Remove unwanted file --- homeassistant/components/tradfri/cover.py | 17 +++++------------ homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 9b831dce0ec..ae7d6a09ce3 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,12 +1,6 @@ """Support for IKEA Tradfri covers.""" -from homeassistant.components.cover import ( - CoverDevice, - ATTR_POSITION, - SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_SET_POSITION, -) +from homeassistant.components.cover import CoverDevice, ATTR_POSITION from .base_class import TradfriBaseDevice from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID @@ -34,11 +28,6 @@ class TradfriCover(TradfriBaseDevice, CoverDevice): self._refresh(device) - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - @property def current_cover_position(self): """Return current position of cover. @@ -59,6 +48,10 @@ class TradfriCover(TradfriBaseDevice, CoverDevice): """Close cover.""" await self._api(self._device_control.set_state(100)) + async def async_stop_cover(self, **kwargs): + """Close cover.""" + await self._api(self._device_control.trigger_blind()) + @property def is_closed(self): """Return if the cover is closed or not.""" diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index d9fa4ad5696..229db67becd 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "Tradfri", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==6.3.1"], + "requirements": ["pytradfri[async]==6.4.0"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 069253d9d23..8ad0f091e66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1612,7 +1612,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==6.3.1 +pytradfri[async]==6.4.0 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb38ac61e2e..25ca8abdbf3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -534,7 +534,7 @@ python_awair==0.0.4 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==6.3.1 +pytradfri[async]==6.4.0 # homeassistant.components.vesync pyvesync==1.1.0 From c96d4c978d078f17b5e207e4febbb6e944a49dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Thu, 24 Oct 2019 20:39:10 +0000 Subject: [PATCH 015/306] Fix tzinfo type for onvif component (#28178) --- homeassistant/components/onvif/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 59ee8a8c7ee..affbbb62338 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -206,7 +206,7 @@ class ONVIFHassCamera(Camera): else: tzone = ( dt_util.get_time_zone(device_time.TimeZone) - or dt_util.DEFAULT_TIME_ZONE, + or dt_util.DEFAULT_TIME_ZONE ) cdate = device_time.LocalDateTime From 98ac8a217d194bfe238d7a2000eea683350c80c8 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Thu, 24 Oct 2019 22:41:07 +0200 Subject: [PATCH 016/306] Adding device_class to samsungtv (#28168) * Adding device_id to samsungtv * Lint * Adding test --- homeassistant/components/samsungtv/media_player.py | 11 ++++++++++- tests/components/samsungtv/test_media_player.py | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 2821a05261b..fd1da31497e 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -6,7 +6,11 @@ import socket import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import ( + MediaPlayerDevice, + PLATFORM_SCHEMA, + DEVICE_CLASS_TV, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -227,6 +231,11 @@ class SamsungTVDevice(MediaPlayerDevice): return SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON return SUPPORT_SAMSUNGTV + @property + def device_class(self): + """Set the device class to TV.""" + return DEVICE_CLASS_TV + def turn_off(self): """Turn off media player.""" self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 9cd5c782b3f..1428ba3b39b 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -8,6 +8,7 @@ from asynctest import mock import pytest import tests.common +from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, MEDIA_TYPE_CHANNEL, @@ -197,6 +198,10 @@ class TestSamsungTv(unittest.TestCase): self.device._mac = "fake" assert SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON == self.device.supported_features + def test_device_class(self): + """Test for device_class property.""" + assert DEVICE_CLASS_TV == self.device.device_class + def test_turn_off(self): """Test for turn_off.""" self.device.send_key = mock.Mock() From 63deec3275552f4d43d2c9cfce9f6853d16c4972 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Thu, 24 Oct 2019 23:12:41 +0200 Subject: [PATCH 017/306] Bump python-slugify to 4.0.0 (#28186) --- 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 d2f10c891a9..0e06690c856 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.6 +python-slugify==4.0.0 pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8ad0f091e66..81009711ed7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.8 pip>=8.0.3 -python-slugify==3.0.6 +python-slugify==4.0.0 pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 diff --git a/setup.py b/setup.py index d2c4934713b..e8b32fd8edf 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ REQUIRES = [ # PyJWT has loose dependency. We want the latest one. "cryptography==2.8", "pip>=8.0.3", - "python-slugify==3.0.6", + "python-slugify==4.0.0", "pytz>=2019.03", "pyyaml==5.1.2", "requests==2.22.0", From 67cf7c26da2b0b7a0dffc1d397cd1396bb6da29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 24 Oct 2019 23:25:47 +0200 Subject: [PATCH 018/306] Removes unwanted tradfri battery sensor (#28181) --- homeassistant/components/tradfri/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 68a2c10291b..cf797f34e3b 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -19,6 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not dev.has_light_control and not dev.has_socket_control and not dev.has_blind_control + and not dev.has_signal_repeater_control ) if devices: async_add_entities(TradfriSensor(device, api, gateway_id) for device in devices) From 32a024c6412df1f45facd5a8afd8cd8dd2f1d596 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 25 Oct 2019 01:41:13 +0200 Subject: [PATCH 019/306] Partially revert tensorflow import move (#28184) * Revert "Refactor imports for tensorflow (#27617)" This reverts commit 5a83a92390e8a3255885198c80622556f886b9b3. * move only some imports to top * fix lint * add comments --- .../components/tensorflow/image_processing.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 1f49888cb95..ea73d52fe4a 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -1,23 +1,12 @@ """Support for performing TensorFlow classification on images.""" +import io import logging import os import sys -import io -import voluptuous as vol + from PIL import Image, ImageDraw import numpy as np - -try: - import cv2 -except ImportError: - cv2 = None - -try: - # Verify that the TensorFlow Object Detection API is pre-installed - import tensorflow as tf # noqa - from object_detection.utils import label_map_util # noqa -except ImportError: - label_map_util = None +import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, @@ -98,8 +87,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # append custom model path to sys.path sys.path.append(model_dir) - os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" - if label_map_util is None: + try: + # Verify that the TensorFlow Object Detection API is pre-installed + # pylint: disable=unused-import,unused-variable + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + # These imports shouldn't be moved to the top, because they depend on code from the model_dir. + # (The model_dir is created during the manual setup process. See integration docs.) + import tensorflow as tf # noqa + from object_detection.utils import label_map_util # noqa + except ImportError: + # pylint: disable=line-too-long _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " @@ -107,7 +104,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # noqa return - if cv2 is None: + try: + # Display warning that PIL will be used if no OpenCV is found. + # pylint: disable=unused-import,unused-variable + import cv2 # noqa + except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " "PIL at reduced resolution" @@ -282,7 +283,13 @@ class TensorFlowImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - if cv2 is None: + try: + import cv2 # pylint: disable=import-error + + img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) + inp = img[:, :, [2, 1, 0]] # BGR->RGB + inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) + except ImportError: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img.thumbnail((460, 460), Image.ANTIALIAS) img_width, img_height = img.size @@ -292,10 +299,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity): .astype(np.uint8) ) inp_expanded = np.expand_dims(inp, axis=0) - else: - img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) - inp = img[:, :, [2, 1, 0]] # BGR->RGB - inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) image_tensor = self._graph.get_tensor_by_name("image_tensor:0") boxes = self._graph.get_tensor_by_name("detection_boxes:0") From 643b3a98ee68757cda35cada5acfc4002c380b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 25 Oct 2019 02:42:54 +0300 Subject: [PATCH 020/306] Huawei LTE sensor metadata update (#28187) --- homeassistant/components/huawei_lte/const.py | 3 ++ homeassistant/components/huawei_lte/sensor.py | 47 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 77126b61c22..18b8d1a90e1 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -7,6 +7,9 @@ DEFAULT_DEVICE_NAME = "LTE" UPDATE_SIGNAL = f"{DOMAIN}_update" UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" +UNIT_BYTES = "B" +UNIT_SECONDS = "s" + KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index e5b65c723f0..99170d4e7c0 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -19,6 +19,8 @@ from .const import ( KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_MONITORING_TRAFFIC_STATISTICS, + UNIT_BYTES, + UNIT_SECONDS, ) @@ -29,7 +31,6 @@ SENSOR_META = { KEY_DEVICE_INFORMATION: dict( include=re.compile(r"^WanIP.*Address$", re.IGNORECASE) ), - (KEY_DEVICE_INFORMATION, "SoftwareVersion"): dict(name="Software version"), (KEY_DEVICE_INFORMATION, "WanIPAddress"): dict( name="WAN IP address", icon="mdi:ip", enabled_default=True ), @@ -38,7 +39,7 @@ SENSOR_META = { ), (KEY_DEVICE_SIGNAL, "band"): dict(name="Band"), (KEY_DEVICE_SIGNAL, "cell_id"): dict(name="Cell ID"), - (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC"), + (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC", icon="mdi:map-marker"), (KEY_DEVICE_SIGNAL, "mode"): dict( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), @@ -96,9 +97,51 @@ SENSOR_META = { or "mdi:signal-cellular-3", enabled_default=True, ), + (KEY_DEVICE_SIGNAL, "rscp"): dict( + name="RSCP", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + # https://wiki.teltonika.lt/view/RSCP + icon=lambda x: (x is None or x < -95) + and "mdi:signal-cellular-outline" + or x < -85 + and "mdi:signal-cellular-1" + or x < -75 + and "mdi:signal-cellular-2" + or "mdi:signal-cellular-3", + ), + (KEY_DEVICE_SIGNAL, "ecio"): dict( + name="EC/IO", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + # https://wiki.teltonika.lt/view/EC/IO + icon=lambda x: (x is None or x < -20) + and "mdi:signal-cellular-outline" + or x < -10 + and "mdi:signal-cellular-1" + or x < -6 + and "mdi:signal-cellular-2" + or "mdi:signal-cellular-3", + ), KEY_MONITORING_TRAFFIC_STATISTICS: dict( exclude=re.compile(r"^showtraffic$", re.IGNORECASE) ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentConnectTime"): dict( + name="Current connection duration", unit=UNIT_SECONDS, icon="mdi:timer" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict( + name="Current connection download", unit=UNIT_BYTES, icon="mdi:download" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): dict( + name="Current connection upload", unit=UNIT_BYTES, icon="mdi:upload" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict( + name="Total connected duration", unit=UNIT_SECONDS, icon="mdi:timer" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): dict( + name="Total download", unit=UNIT_BYTES, icon="mdi:download" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): dict( + name="Total upload", unit=UNIT_BYTES, icon="mdi:upload" + ), } From 95295791bd4529d0e9435e03a46f4091e884fc04 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 25 Oct 2019 00:32:15 +0000 Subject: [PATCH 021/306] [ci skip] Translation update --- .../components/abode/.translations/pt-BR.json | 9 +++++ .../.translations/pt-BR.json | 4 +++ .../components/axis/.translations/pt-BR.json | 1 + .../binary_sensor/.translations/hu.json | 10 +++--- .../binary_sensor/.translations/ru.json | 4 +-- .../cert_expiry/.translations/zh-Hant.json | 4 ++- .../coolmaster/.translations/ca.json | 12 +++++++ .../coolmaster/.translations/da.json | 15 ++++++++ .../coolmaster/.translations/fr.json | 23 ++++++++++++ .../coolmaster/.translations/no.json | 23 ++++++++++++ .../coolmaster/.translations/pt-BR.json | 17 +++++++++ .../coolmaster/.translations/zh-Hant.json | 23 ++++++++++++ .../components/deconz/.translations/ca.json | 2 +- .../dialogflow/.translations/ru.json | 2 +- .../components/geofency/.translations/ru.json | 2 +- .../components/glances/.translations/ca.json | 2 +- .../glances/.translations/pt-BR.json | 20 +++++++++++ .../gpslogger/.translations/ru.json | 2 +- .../huawei_lte/.translations/ca.json | 28 +++++++++++++++ .../components/ifttt/.translations/ru.json | 2 +- .../components/locative/.translations/ru.json | 2 +- .../components/lock/.translations/pt-BR.json | 9 +++++ .../components/mailgun/.translations/ru.json | 2 +- .../mobile_app/.translations/ru.json | 2 +- .../components/nest/.translations/ru.json | 2 +- .../opentherm_gw/.translations/pt-BR.json | 11 ++++++ .../owntracks/.translations/ru.json | 2 +- .../components/plaato/.translations/ru.json | 2 +- .../components/plex/.translations/ca.json | 2 +- .../components/ps4/.translations/ru.json | 2 +- .../components/sensor/.translations/ru.json | 11 ++++++ .../sensor/.translations/zh-Hant.json | 36 +++++++++---------- .../solarlog/.translations/zh-Hant.json | 21 +++++++++++ .../components/soma/.translations/ru.json | 2 +- .../components/somfy/.translations/ru.json | 2 +- .../components/traccar/.translations/ru.json | 2 +- .../transmission/.translations/ca.json | 5 ++- .../transmission/.translations/da.json | 5 ++- .../transmission/.translations/fr.json | 5 ++- .../transmission/.translations/no.json | 5 ++- .../transmission/.translations/pt-BR.json | 5 ++- .../transmission/.translations/zh-Hant.json | 5 ++- .../components/twilio/.translations/ru.json | 2 +- .../components/upnp/.translations/ca.json | 2 +- .../components/withings/.translations/ca.json | 6 ++++ .../components/withings/.translations/en.json | 7 ++++ .../components/zwave/.translations/ru.json | 2 +- 47 files changed, 313 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/ca.json create mode 100644 homeassistant/components/coolmaster/.translations/da.json create mode 100644 homeassistant/components/coolmaster/.translations/fr.json create mode 100644 homeassistant/components/coolmaster/.translations/no.json create mode 100644 homeassistant/components/coolmaster/.translations/pt-BR.json create mode 100644 homeassistant/components/coolmaster/.translations/zh-Hant.json create mode 100644 homeassistant/components/glances/.translations/pt-BR.json create mode 100644 homeassistant/components/huawei_lte/.translations/ca.json create mode 100644 homeassistant/components/lock/.translations/pt-BR.json create mode 100644 homeassistant/components/opentherm_gw/.translations/pt-BR.json create mode 100644 homeassistant/components/solarlog/.translations/zh-Hant.json diff --git a/homeassistant/components/abode/.translations/pt-BR.json b/homeassistant/components/abode/.translations/pt-BR.json index 7a117a81993..30980103b38 100644 --- a/homeassistant/components/abode/.translations/pt-BR.json +++ b/homeassistant/components/abode/.translations/pt-BR.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o de Abode \u00e9 permitida." + }, + "error": { + "connection_error": "N\u00e3o foi poss\u00edvel conectar ao Abode.", + "identifier_exists": "Conta j\u00e1 cadastrada.", + "invalid_credentials": "Credenciais inv\u00e1lidas." + }, "step": { "user": { "data": { + "password": "Senha", "username": "Endere\u00e7o de e-mail" } } diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json index 1f7c994330d..156ede8851b 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -1,6 +1,10 @@ { "device_automation": { "action_type": { + "arm_away": "Armar {entity_name} longe", + "arm_home": "Armar {entity_name} casa", + "arm_night": "Armar {entity_name} noite", + "disarm": "Desarmar {entity_name}", "trigger": "Disparar {entidade_nome}" } } diff --git a/homeassistant/components/axis/.translations/pt-BR.json b/homeassistant/components/axis/.translations/pt-BR.json index 0b8fe8541da..453c8fa3643 100644 --- a/homeassistant/components/axis/.translations/pt-BR.json +++ b/homeassistant/components/axis/.translations/pt-BR.json @@ -12,6 +12,7 @@ "device_unavailable": "O dispositivo n\u00e3o est\u00e1 dispon\u00edvel", "faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas" }, + "flow_title": "Eixos do dispositivo: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/hu.json b/homeassistant/components/binary_sensor/.translations/hu.json index e53d918f98d..7ec9b5268e2 100644 --- a/homeassistant/components/binary_sensor/.translations/hu.json +++ b/homeassistant/components/binary_sensor/.translations/hu.json @@ -46,13 +46,14 @@ }, "trigger_type": { "bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", - "closed": "{entity_name} be lett z\u00e1rva", + "closed": "{entity_name} be lett csukva", "cold": "{entity_name} hideg lett", - "connected": "{entity_name} csatlakozott", + "connected": "{entity_name} csatlakozik", "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", - "hot": "{entity_name} felforr\u00f3sodott", + "hot": "{entity_name} felforr\u00f3sodik", "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", "locked": "{entity_name} be lett z\u00e1rva", + "moist": "{entity_name} nedves lett", "moist\u00a7": "{entity_name} nedves lett", "motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", "moving": "{entity_name} mozog", @@ -65,12 +66,13 @@ "no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st", "not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", "not_cold": "{entity_name} m\u00e1r nem hideg", - "not_connected": "{entity_name} lecsatlakozott", + "not_connected": "{entity_name} lecsatlakozik", "not_hot": "{entity_name} m\u00e1r nem forr\u00f3", "not_locked": "{entity_name} ki lett nyitva", "not_moist": "{entity_name} sz\u00e1raz lett", "not_moving": "{entity_name} m\u00e1r nem mozog", "not_occupied": "{entity_name} m\u00e1r nem foglalt", + "not_opened": "{entity_name} be lett csukva", "not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva", "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", "not_present": "{entity_name} m\u00e1r nincs jelen", diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index cce765c8d84..4c9cfb99a1c 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -28,7 +28,7 @@ "is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", @@ -36,7 +36,7 @@ "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hant.json b/homeassistant/components/cert_expiry/.translations/zh-Hant.json index 9af730db969..c710deae5c1 100644 --- a/homeassistant/components/cert_expiry/.translations/zh-Hant.json +++ b/homeassistant/components/cert_expiry/.translations/zh-Hant.json @@ -4,10 +4,12 @@ "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { + "certificate_error": "\u8a8d\u8b49\u7121\u6cd5\u78ba\u8a8d", "certificate_fetch_failed": "\u7121\u6cd5\u81ea\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u7372\u5f97\u8a8d\u8b49", "connection_timeout": "\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef\u903e\u6642", "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790" + "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790", + "wrong_host": "\u8a8d\u8b49\u8207\u4e3b\u6a5f\u540d\u7a31\u4e0d\u7b26\u5408" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/ca.json b/homeassistant/components/coolmaster/.translations/ca.json new file mode 100644 index 00000000000..c79397b0cc5 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/ca.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/da.json b/homeassistant/components/coolmaster/.translations/da.json new file mode 100644 index 00000000000..8f50a0eb6dd --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "heat_cool": "Underst\u00f8t automatisk varm/k\u00f8l tilstand", + "host": "V\u00e6rt", + "off": "Kan slukkes" + }, + "title": "Ops\u00e6t dine CoolMasterNet-forbindelsesdetaljer." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/fr.json b/homeassistant/components/coolmaster/.translations/fr.json new file mode 100644 index 00000000000..97b1753ddde --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "\u00c9chec de la connexion \u00e0 l'instance CoolMasterNet. S'il vous pla\u00eet v\u00e9rifier votre h\u00f4te.", + "no_units": "Impossible de trouver des unit\u00e9s HVAC dans l'h\u00f4te CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Prise en charge du mode refroidissement", + "dry": "Prise en charge du mode d\u00e9shumidification", + "fan_only": "Prise en charge du mode ventilateur uniquement", + "heat": "Prise en charge du mode chauffage", + "heat_cool": "Prise en charge du mode chauffage / refroidissement automatique", + "host": "H\u00f4te", + "off": "Peut \u00eatre \u00e9teint" + }, + "title": "Configurez les d\u00e9tails de votre connexion CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/no.json b/homeassistant/components/coolmaster/.translations/no.json new file mode 100644 index 00000000000..90c40aaa617 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Kunne ikke koble til CoolMasterNet-forekomsten. Sjekk verten din.", + "no_units": "Kunne ikke finne noen HVAC-enheter i CoolMasterNet vert." + }, + "step": { + "user": { + "data": { + "cool": "St\u00f8tte kj\u00f8lemodus", + "dry": "St\u00f8tt t\u00f8rr modus", + "fan_only": "St\u00f8tt kun modus for vifte", + "heat": "St\u00f8tt varmemodus", + "heat_cool": "St\u00f8tter automatisk varme/kj\u00f8l-modus", + "host": "Vert", + "off": "Kan sl\u00e5s av" + }, + "title": "Konfigurer informasjonen om CoolMasterNet-tilkoblingen." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/pt-BR.json b/homeassistant/components/coolmaster/.translations/pt-BR.json new file mode 100644 index 00000000000..bb821341818 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cool": "Suporta o modo de resfriamento", + "dry": "Suporta o modo seco", + "fan_only": "Suporte apenas o modo ventilador", + "heat": "Suporta o modo de aquecimento", + "heat_cool": "Suporta o modo de aquecimento/resfriamento autom\u00e1tico", + "host": "Host", + "off": "Pode ser desligado" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/zh-Hant.json b/homeassistant/components/coolmaster/.translations/zh-Hant.json new file mode 100644 index 00000000000..bc61e82b98a --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "\u9023\u7dda\u81f3 CoolMasterNet \u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u3002", + "no_units": "\u7121\u6cd5\u65bc CoolMasterNet \u4e3b\u6a5f\u627e\u5230\u4efb\u4f55 HVAC \u8a2d\u5099\u3002" + }, + "step": { + "user": { + "data": { + "cool": "\u652f\u63f4\u5236\u51b7\u6a21\u5f0f", + "dry": "\u652f\u63f4\u9664\u6fd5\u6a21\u5f0f", + "fan_only": "\u652f\u63f4\u50c5\u9001\u98a8\u6a21\u5f0f", + "heat": "\u652f\u63f4\u4fdd\u6696\u6a21\u5f0f", + "heat_cool": "\u652f\u63f4\u81ea\u52d5\u4fdd\u6696/\u5236\u51b7\u6a21\u5f0f", + "host": "\u4e3b\u6a5f\u7aef", + "off": "\u53ef\u4ee5\u95dc\u9589" + }, + "title": "\u8a2d\u5b9a CoolMasterNet \u9023\u7dda\u8cc7\u8a0a\u3002" + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index a2facf0d7c2..41f257e2a78 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -37,7 +37,7 @@ "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", "allow_deconz_groups": "Permetre la importaci\u00f3 de grups deCONZ" }, - "title": "Opcions de configuraci\u00f3 addicionals per deCONZ" + "title": "Opcions de configuraci\u00f3 addicionals de deCONZ" } }, "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" diff --git a/homeassistant/components/dialogflow/.translations/ru.json b/homeassistant/components/dialogflow/.translations/ru.json index d8b7db09a78..88405328896 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 \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." + "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 Webhook \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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/ru.json b/homeassistant/components/geofency/.translations/ru.json index 6c699d21ce6..3663ff0114c 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 \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." + "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 Webhook \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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/glances/.translations/ca.json b/homeassistant/components/glances/.translations/ca.json index edff236623e..2610fe156aa 100644 --- a/homeassistant/components/glances/.translations/ca.json +++ b/homeassistant/components/glances/.translations/ca.json @@ -30,7 +30,7 @@ "data": { "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" }, - "description": "Opcions de configuraci\u00f3 per a Glances" + "description": "Opcions de configuraci\u00f3 de Glances" } } } diff --git a/homeassistant/components/glances/.translations/pt-BR.json b/homeassistant/components/glances/.translations/pt-BR.json new file mode 100644 index 00000000000..05ea657c8b3 --- /dev/null +++ b/homeassistant/components/glances/.translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ru.json b/homeassistant/components/gpslogger/.translations/ru.json index 366cb1735d5..b33bde95aec 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 \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." + "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 Webhook \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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json new file mode 100644 index 00000000000..5b31875586d --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest dispositiu ja est\u00e0 configurat" + }, + "error": { + "connection_failed": "La connexi\u00f3 ha fallat", + "incorrect_password": "Contrasenya incorrecta", + "incorrect_username": "Nom d'usuari incorrecte", + "incorrect_username_or_password": "Nom d'usuari o contrasenya incorrectes", + "invalid_url": "URL inv\u00e0lid", + "login_attempts_exceeded": "Nombre m\u00e0xim d'intents d'inici de sessi\u00f3 superat, torna-ho a provar m\u00e9s tard", + "response_error": "S'ha produ\u00eft un error desconegut del dispositiu", + "unknown_connection_error": "S'ha produ\u00eft un error desconegut en connectar-se al dispositiu" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "url": "URL", + "username": "Nom d'usuari" + }, + "title": "Con de Huawei LTE" + } + }, + "title": "Huawei LTE" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ru.json b/homeassistant/components/ifttt/.translations/ru.json index ae5fdbab3f6..128db247150 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 \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." + "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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/locative/.translations/ru.json b/homeassistant/components/locative/.translations/ru.json index 70f08595f3a..90fa5253a61 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 \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." + "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 Webhook \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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/lock/.translations/pt-BR.json b/homeassistant/components/lock/.translations/pt-BR.json new file mode 100644 index 00000000000..f6bde89a3a6 --- /dev/null +++ b/homeassistant/components/lock/.translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "action_type": { + "lock": "Bloquear {entity_name}", + "open": "Abrir {entity_name}", + "unlock": "Desbloquear {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json index 094940e6f90..9e8a293b4bf 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 \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." + "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 Webhook \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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/mobile_app/.translations/ru.json b/homeassistant/components/mobile_app/.translations/ru.json index 202b7383253..28a497ef219 100644 --- a/homeassistant/components/mobile_app/.translations/ru.json +++ b/homeassistant/components/mobile_app/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0441 Home Assistant. \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]({apps_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u044b\u0445 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439." + "install_app": "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0441 Home Assistant. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({apps_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u044b\u0445 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439." }, "step": { "confirm": { diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index ba49b788b9a..9abdafafa93 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -22,7 +22,7 @@ }, "link": { "data": { - "code": "\u041f\u0438\u043d-\u043a\u043e\u0434" + "code": "PIN-\u043a\u043e\u0434" }, "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest" diff --git a/homeassistant/components/opentherm_gw/.translations/pt-BR.json b/homeassistant/components/opentherm_gw/.translations/pt-BR.json new file mode 100644 index 00000000000..96907fd4cdc --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "precision": "Precis\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/ru.json b/homeassistant/components/owntracks/.translations/ru.json index 31c3e77279d..0e9479c1ed4 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 \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\u043e\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\u043e\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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/plaato/.translations/ru.json b/homeassistant/components/plaato/.translations/ru.json index dc06e3ddab0..fc1255ed9ce 100644 --- a/homeassistant/components/plaato/.translations/ru.json +++ b/homeassistant/components/plaato/.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 Plaato Airlock.\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 \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 7a8cf7a1424..9eb6d16639f 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -55,7 +55,7 @@ "show_all_controls": "Mostra tots els controls", "use_episode_art": "Utilitza imatges de l'episodi" }, - "description": "Opcions per als reproductors multim\u00e8dia Plex" + "description": "Opcions dels reproductors multim\u00e8dia Plex" } } } diff --git a/homeassistant/components/ps4/.translations/ru.json b/homeassistant/components/ps4/.translations/ru.json index b50d4bb838f..bf2484d0254 100644 --- a/homeassistant/components/ps4/.translations/ru.json +++ b/homeassistant/components/ps4/.translations/ru.json @@ -25,7 +25,7 @@ "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**. \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](https://www.home-assistant.io/components/ps4/) \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.", + "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**. \u041e\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/ps4/) \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.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/sensor/.translations/ru.json b/homeassistant/components/sensor/.translations/ru.json index 8c70f41fcb7..0f2c1bc0e4e 100644 --- a/homeassistant/components/sensor/.translations/ru.json +++ b/homeassistant/components/sensor/.translations/ru.json @@ -1,5 +1,16 @@ { "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_illuminance": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_pressure": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_signal_strength": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_temperature": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_timestamp": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_value": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + }, "trigger_type": { "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/sensor/.translations/zh-Hant.json b/homeassistant/components/sensor/.translations/zh-Hant.json index af97681ee76..eb8f47a1fd9 100644 --- a/homeassistant/components/sensor/.translations/zh-Hant.json +++ b/homeassistant/components/sensor/.translations/zh-Hant.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} \u96fb\u91cf", - "is_humidity": "{entity_name} \u6fd5\u5ea6", - "is_illuminance": "{entity_name} \u7167\u5ea6", - "is_power": "{entity_name} \u96fb\u529b", - "is_pressure": "{entity_name} \u58d3\u529b", - "is_signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", - "is_temperature": "{entity_name} \u6eab\u5ea6", - "is_timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", - "is_value": "{entity_name} \u503c" + "is_battery_level": "\u76ee\u524d {entity_name} \u96fb\u91cf", + "is_humidity": "\u76ee\u524d {entity_name} \u6fd5\u5ea6", + "is_illuminance": "\u76ee\u524d {entity_name} \u7167\u5ea6", + "is_power": "\u76ee\u524d {entity_name} \u96fb\u529b", + "is_pressure": "\u76ee\u524d {entity_name} \u58d3\u529b", + "is_signal_strength": "\u76ee\u524d {entity_name} \u8a0a\u865f\u5f37\u5ea6", + "is_temperature": "\u76ee\u524d {entity_name} \u6eab\u5ea6", + "is_timestamp": "\u76ee\u524d {entity_name} \u6642\u9593\u6a19\u8a18", + "is_value": "\u76ee\u524d {entity_name} \u503c" }, "trigger_type": { - "battery_level": "{entity_name} \u96fb\u91cf", - "humidity": "{entity_name} \u6fd5\u5ea6", - "illuminance": "{entity_name} \u7167\u5ea6", - "power": "{entity_name} \u96fb\u529b", - "pressure": "{entity_name} \u58d3\u529b", - "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", - "temperature": "{entity_name} \u6eab\u5ea6", - "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", - "value": "{entity_name} \u503c" + "battery_level": "{entity_name} \u96fb\u91cf\u8b8a\u66f4", + "humidity": "{entity_name} \u6fd5\u5ea6\u8b8a\u66f4", + "illuminance": "{entity_name} \u7167\u5ea6\u8b8a\u66f4", + "power": "{entity_name} \u96fb\u529b\u8b8a\u66f4", + "pressure": "{entity_name} \u58d3\u529b\u8b8a\u66f4", + "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", + "temperature": "{entity_name} \u6eab\u5ea6\u8b8a\u66f4", + "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18\u8b8a\u66f4", + "value": "{entity_name} \u503c\u8b8a\u66f4" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/zh-Hant.json b/homeassistant/components/solarlog/.translations/zh-Hant.json new file mode 100644 index 00000000000..19ec431d2ca --- /dev/null +++ b/homeassistant/components/solarlog/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u78ba\u8a8d\u4e3b\u6a5f\u4f4d\u5740" + }, + "step": { + "user": { + "data": { + "host": "Solar-Log \u8a2d\u5099\u4e4b\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "name": "Solar-Log \u50b3\u611f\u5668\u6240\u4f7f\u7528\u5b57\u9996" + }, + "title": "\u5b9a\u7fa9 Solar-Log \u9023\u7dda" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json index f7e6574b113..f28d672d3f2 100644 --- a/homeassistant/components/soma/.translations/ru.json +++ b/homeassistant/components/soma/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "\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.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\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." + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \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." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/somfy/.translations/ru.json b/homeassistant/components/somfy/.translations/ru.json index 7251bc990e9..0c8778dc2e5 100644 --- a/homeassistant/components/somfy/.translations/ru.json +++ b/homeassistant/components/somfy/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "\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.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Somfy \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\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." + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Somfy \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \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." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/traccar/.translations/ru.json b/homeassistant/components/traccar/.translations/ru.json index afaab87efe4..1a215c90d4b 100644 --- a/homeassistant/components/traccar/.translations/ru.json +++ b/homeassistant/components/traccar/.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 \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Traccar.\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\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 Webhook \u0434\u043b\u044f Traccar.\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\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/transmission/.translations/ca.json b/homeassistant/components/transmission/.translations/ca.json index 395f6e2d681..f621574683f 100644 --- a/homeassistant/components/transmission/.translations/ca.json +++ b/homeassistant/components/transmission/.translations/ca.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat.", "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "error": { "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", + "name_exists": "El nom ja existeix", "wrong_credentials": "Nom d'usuari o contrasenya incorrectes" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" }, - "description": "Opcions de configuraci\u00f3 per a Transmission" + "description": "Opcions de configuraci\u00f3 de Transmission", + "title": "Opcions de configuraci\u00f3 de Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json index 3a619d8154a..b14fca00c2c 100644 --- a/homeassistant/components/transmission/.translations/da.json +++ b/homeassistant/components/transmission/.translations/da.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "V\u00e6rten er allerede konfigureret.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", + "name_exists": "Navnet findes allerede", "wrong_credentials": "Ugyldigt brugernavn eller adgangskode" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Opdateringsfrekvens" }, - "description": "Konfigurationsindstillinger for Transmission" + "description": "Konfigurationsindstillinger for Transmission", + "title": "Konfigurationsindstillinger for Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/fr.json b/homeassistant/components/transmission/.translations/fr.json index e2360c016ca..3c267b36a08 100644 --- a/homeassistant/components/transmission/.translations/fr.json +++ b/homeassistant/components/transmission/.translations/fr.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9.", "one_instance_allowed": "Une seule instance est n\u00e9cessaire." }, "error": { "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9", "wrong_credentials": "Mauvais nom d'utilisateur ou mot de passe" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" }, - "description": "Configurer les options pour Transmission" + "description": "Configurer les options pour Transmission", + "title": "Configurer les options pour Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json index 94044e692d9..9cd19cd87f8 100644 --- a/homeassistant/components/transmission/.translations/no.json +++ b/homeassistant/components/transmission/.translations/no.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Verten er allerede konfigurert.", "one_instance_allowed": "Bare en enkel instans er n\u00f8dvendig." }, "error": { "cannot_connect": "Kan ikke koble til vert", + "name_exists": "Navnet eksisterer allerede", "wrong_credentials": "Ugyldig brukernavn eller passord" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Oppdater frekvens" }, - "description": "Konfigurer alternativer for Transmission" + "description": "Konfigurer alternativer for Transmission", + "title": "Konfigurer alternativer for Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/pt-BR.json b/homeassistant/components/transmission/.translations/pt-BR.json index cabbb6d9149..de854e1273c 100644 --- a/homeassistant/components/transmission/.translations/pt-BR.json +++ b/homeassistant/components/transmission/.translations/pt-BR.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "O host j\u00e1 est\u00e1 configurado.", "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." }, "error": { "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", + "name_exists": "O nome j\u00e1 existe", "wrong_credentials": "Nome de usu\u00e1rio ou senha incorretos" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" }, - "description": "Configurar op\u00e7\u00f5es para transmiss\u00e3o" + "description": "Configurar op\u00e7\u00f5es para transmiss\u00e3o", + "title": "Configurar op\u00e7\u00f5es para Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/zh-Hant.json b/homeassistant/components/transmission/.translations/zh-Hant.json index 479e25c6d8a..304babc991e 100644 --- a/homeassistant/components/transmission/.translations/zh-Hant.json +++ b/homeassistant/components/transmission/.translations/zh-Hant.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "error": { "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", "wrong_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "\u66f4\u65b0\u983b\u7387" }, - "description": "Transmission \u8a2d\u5b9a\u9078\u9805" + "description": "Transmission \u8a2d\u5b9a\u9078\u9805", + "title": "Transmission \u8a2d\u5b9a\u9078\u9805" } } } diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json index 1c4e0653496..ba8ed6179d4 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 \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." + "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 Webhook \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 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({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/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json index 28ad9ce954d..85370eec8e6 100644 --- a/homeassistant/components/upnp/.translations/ca.json +++ b/homeassistant/components/upnp/.translations/ca.json @@ -26,7 +26,7 @@ "enable_sensors": "Afegeix sensors de tr\u00e0nsit", "igd": "UPnP/IGD" }, - "title": "Opcions de configuraci\u00f3 per a UPnP/IGD" + "title": "Opcions de configuraci\u00f3 d'UPnP/IGD" } }, "title": "UPnP/IGD" diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index 2f2fdbe9b3f..89dae4caa1b 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -7,6 +7,12 @@ "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." }, "step": { + "profile": { + "data": { + "profile": "Perfil" + }, + "title": "Perfil d'usuari." + }, "user": { "data": { "profile": "Perfil" diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json index 16ce491e776..987e3347a99 100644 --- a/homeassistant/components/withings/.translations/en.json +++ b/homeassistant/components/withings/.translations/en.json @@ -7,6 +7,13 @@ "default": "Successfully authenticated with Withings for the selected profile." }, "step": { + "profile": { + "data": { + "profile": "Profile" + }, + "description": "Which profile did you select on the Withings website? It's important the profiles match, otherwise data will be mis-labeled.", + "title": "User Profile." + }, "user": { "data": { "profile": "Profile" diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index 4243f583082..a1039c2dedc 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -13,7 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\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](https://www.home-assistant.io/docs/z-wave/installation/) \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 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "description": "\u041e\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/docs/z-wave/installation/) \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 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Z-Wave" } }, From 9661efc31275280540940a5700703778c9d5f3ae Mon Sep 17 00:00:00 2001 From: escoand Date: Fri, 25 Oct 2019 14:32:12 +0200 Subject: [PATCH 022/306] Add Samsung TV automatic protocol detection (#27492) * added automatic protocol detection * fix logger tests * fix async tests * add missin const.py * fix log formatting * wait for first update call * migrate first tests * migrated all test functions * started to use state machine * updated all tests to use async_setup_component * slove hints * update tests * get state at correct position * remove impossible tests * fix autodetect tests * use caplog fixture * add test for duplicate * catch concrete exceptions * don't mock samsungctl exceptions * add test for discovery * get state when possible * add test for autodetect without connection --- CODEOWNERS | 1 + .../components/samsungtv/__init__.py | 2 +- homeassistant/components/samsungtv/const.py | 5 + .../components/samsungtv/manifest.json | 4 +- .../components/samsungtv/media_player.py | 76 +- .../components/samsungtv/test_media_player.py | 934 ++++++++++++------ 6 files changed, 689 insertions(+), 333 deletions(-) create mode 100644 homeassistant/components/samsungtv/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 809101a5271..40e37ec4697 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -247,6 +247,7 @@ homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt homeassistant/components/saj/* @fredericvl +homeassistant/components/samsungtv/* @escoand homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index e43ea1ba984..6b4f0e31f02 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1 +1 @@ -"""The samsungtv component.""" +"""The Samsung TV integration.""" diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py new file mode 100644 index 00000000000..83d74743844 --- /dev/null +++ b/homeassistant/components/samsungtv/const.py @@ -0,0 +1,5 @@ +"""Constants for the Samsung TV integration.""" +import logging + +LOGGER = logging.getLogger(__package__) +DOMAIN = "samsungtv" diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index a080fac112a..405d757cbef 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -1,11 +1,11 @@ { "domain": "samsungtv", - "name": "Samsungtv", + "name": "Samsung TV", "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", "wakeonlan==1.1.6" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@escoand"] } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index fd1da31497e..94e9131ed32 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -1,7 +1,6 @@ """Support for interface with an Samsung TV.""" import asyncio from datetime import timedelta -import logging import socket import voluptuous as vol @@ -36,14 +35,14 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util -_LOGGER = logging.getLogger(__name__) +from .const import LOGGER DEFAULT_NAME = "Samsung TV Remote" -DEFAULT_PORT = 55000 DEFAULT_TIMEOUT = 1 KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = "samsungtv_known_devices" +METHODS = ("websocket", "legacy") SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} SUPPORT_SAMSUNGTV = ( @@ -62,7 +61,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_PORT): cv.port, vol.Optional(CONF_MAC): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, } @@ -89,15 +88,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): model = discovery_info.get("model_name") host = discovery_info.get("host") name = f"{tv_name} ({model})" - port = DEFAULT_PORT + if name.startswith("[TV]"): + name = name[4:] + port = None timeout = DEFAULT_TIMEOUT mac = None - udn = discovery_info.get("udn") - if udn and udn.startswith("uuid:"): - uuid = udn[len("uuid:") :] - else: - _LOGGER.warning("Cannot determine device") - return + uuid = discovery_info.get("udn") + if uuid and uuid.startswith("uuid:"): + uuid = uuid[len("uuid:") :] # Only add a device once, so discovered devices do not override manual # config. @@ -105,9 +103,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if ip_addr not in known_devices: known_devices.add(ip_addr) add_entities([SamsungTVDevice(host, port, name, timeout, mac, uuid)]) - _LOGGER.info("Samsung TV %s:%d added as '%s'", host, port, name) + LOGGER.info("Samsung TV %s added as '%s'", host, name) else: - _LOGGER.info("Ignoring duplicate Samsung TV %s:%d", host, port) + LOGGER.info("Ignoring duplicate Samsung TV %s", host) class SamsungTVDevice(MediaPlayerDevice): @@ -140,14 +138,16 @@ class SamsungTVDevice(MediaPlayerDevice): "name": "HomeAssistant", "description": name, "id": "ha.component.samsung", + "method": None, "port": port, "host": host, "timeout": timeout, } + # Select method by port number, mainly for fallback if self._config["port"] in (8001, 8002): self._config["method"] = "websocket" - else: + elif self._config["port"] == 55000: self._config["method"] = "legacy" def update(self): @@ -156,16 +156,47 @@ class SamsungTVDevice(MediaPlayerDevice): def get_remote(self): """Create or return a remote control instance.""" + + # Try to find correct method automatically + if self._config["method"] not in METHODS: + for method in METHODS: + try: + self._config["method"] = method + LOGGER.debug("Try config: %s", self._config) + self._remote = self._remote_class(self._config.copy()) + self._state = STATE_ON + LOGGER.debug("Found working config: %s", self._config) + break + except ( + self._exceptions_class.UnhandledResponse, + self._exceptions_class.AccessDenied, + ): + # We got a response so it's working. + self._state = STATE_ON + LOGGER.debug( + "Found working config without connection: %s", self._config + ) + break + except OSError as err: + LOGGER.debug("Failing config: %s error was: %s", self._config, err) + self._config["method"] = None + + # Unable to find working connection + if self._config["method"] is None: + self._remote = None + self._state = None + return None + if self._remote is None: # We need to create a new instance to reconnect. - self._remote = self._remote_class(self._config) + self._remote = self._remote_class(self._config.copy()) return self._remote def send_key(self, key): """Send a key to the tv and handles exceptions.""" if self._power_off_in_progress() and key not in ("KEY_POWER", "KEY_POWEROFF"): - _LOGGER.info("TV is powering off, not sending command: %s", key) + LOGGER.info("TV is powering off, not sending command: %s", key) return try: # recreate connection if connection was dead @@ -178,6 +209,9 @@ class SamsungTVDevice(MediaPlayerDevice): # BrokenPipe can occur when the commands is sent to fast self._remote = None self._state = STATE_ON + except AttributeError: + # Auto-detect could not find working config yet + pass except ( self._exceptions_class.UnhandledResponse, self._exceptions_class.AccessDenied, @@ -185,7 +219,7 @@ class SamsungTVDevice(MediaPlayerDevice): # We got a response so it's on. self._state = STATE_ON self._remote = None - _LOGGER.debug("Failed sending command %s", key, exc_info=True) + LOGGER.debug("Failed sending command %s", key, exc_info=True) return except OSError: self._state = STATE_OFF @@ -249,7 +283,7 @@ class SamsungTVDevice(MediaPlayerDevice): self.get_remote().close() self._remote = None except OSError: - _LOGGER.debug("Could not establish connection.") + LOGGER.debug("Could not establish connection.") def volume_up(self): """Volume up the media player.""" @@ -291,14 +325,14 @@ class SamsungTVDevice(MediaPlayerDevice): async def async_play_media(self, media_type, media_id, **kwargs): """Support changing a channel.""" if media_type != MEDIA_TYPE_CHANNEL: - _LOGGER.error("Unsupported media type") + LOGGER.error("Unsupported media type") return # media_id should only be a channel number try: cv.positive_int(media_id) except vol.Invalid: - _LOGGER.error("Media ID must be positive integer") + LOGGER.error("Media ID must be positive integer") return for digit in media_id: @@ -316,7 +350,7 @@ class SamsungTVDevice(MediaPlayerDevice): async def async_select_source(self, source): """Select input source.""" if source not in SOURCES: - _LOGGER.error("Unsupported source") + LOGGER.error("Unsupported source") return await self.hass.async_add_job(self.send_key, SOURCES[source]) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 1428ba3b39b..c178710e3f9 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,309 +1,573 @@ """Tests for samsungtv Components.""" import asyncio -import unittest -from unittest.mock import call, patch, MagicMock - from asynctest import mock - +from datetime import timedelta import pytest +from samsungctl import exceptions +from tests.common import MockDependency, async_fire_time_changed +from unittest.mock import call, patch -import tests.common from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_VOLUME_MUTED, + DOMAIN, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOURCE, SUPPORT_TURN_ON, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL, ) +from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN from homeassistant.components.samsungtv.media_player import ( - setup_platform, CONF_TIMEOUT, - SamsungTVDevice, SUPPORT_SAMSUNGTV, ) from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_ON, CONF_MAC, + CONF_NAME, + CONF_PLATFORM, + CONF_PORT, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_UP, STATE_OFF, + STATE_ON, + STATE_UNKNOWN, ) -from tests.common import MockDependency -from homeassistant.util import dt as dt_util -from datetime import timedelta +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -WORKING_CONFIG = { - CONF_HOST: "fake", - CONF_NAME: "fake", - CONF_PORT: 8001, - CONF_TIMEOUT: 10, - CONF_MAC: "fake", - "uuid": None, + +ENTITY_ID = f"{DOMAIN}.fake" +MOCK_CONFIG = { + DOMAIN: { + CONF_PLATFORM: SAMSUNGTV_DOMAIN, + CONF_HOST: "fake", + CONF_NAME: "fake", + CONF_PORT: 8001, + CONF_TIMEOUT: 10, + CONF_MAC: "fake", + } } -DISCOVERY_INFO = {"name": "fake", "model_name": "fake", "host": "fake"} +ENTITY_ID_NOMAC = f"{DOMAIN}.fake_nomac" +MOCK_CONFIG_NOMAC = { + DOMAIN: { + CONF_PLATFORM: SAMSUNGTV_DOMAIN, + CONF_HOST: "fake_nomac", + CONF_NAME: "fake_nomac", + CONF_PORT: 55000, + CONF_TIMEOUT: 10, + } +} + +ENTITY_ID_AUTO = f"{DOMAIN}.fake_auto" +MOCK_CONFIG_AUTO = { + DOMAIN: { + CONF_PLATFORM: SAMSUNGTV_DOMAIN, + CONF_HOST: "fake_auto", + CONF_NAME: "fake_auto", + } +} + +ENTITY_ID_DISCOVERY = f"{DOMAIN}.fake_discovery_fake_model" +MOCK_CONFIG_DISCOVERY = { + "name": "fake_discovery", + "model_name": "fake_model", + "host": "fake_host", + "udn": "fake_uuid", +} + +ENTITY_ID_DISCOVERY_PREFIX = f"{DOMAIN}.fake_discovery_prefix_fake_model_prefix" +MOCK_CONFIG_DISCOVERY_PREFIX = { + "name": "[TV]fake_discovery_prefix", + "model_name": "fake_model_prefix", + "host": "fake_host_prefix", + "udn": "uuid:fake_uuid_prefix", +} + +AUTODETECT_WEBSOCKET = { + "name": "HomeAssistant", + "description": "fake_auto", + "id": "ha.component.samsung", + "method": "websocket", + "port": None, + "host": "fake_auto", + "timeout": 1, +} +AUTODETECT_LEGACY = { + "name": "HomeAssistant", + "description": "fake_auto", + "id": "ha.component.samsung", + "method": "legacy", + "port": None, + "host": "fake_auto", + "timeout": 1, +} -class AccessDenied(Exception): - """Dummy Exception.""" +@pytest.fixture(name="remote") +def remote_fixture(): + """Patch the samsungctl Remote.""" + with patch("samsungctl.Remote") as remote_class, patch( + "homeassistant.components.samsungtv.media_player.socket" + ) as socket_class: + remote = mock.Mock() + remote_class.return_value = remote + socket = mock.Mock() + socket_class.return_value = socket + yield remote -class ConnectionClosed(Exception): - """Dummy Exception.""" - - -class UnhandledResponse(Exception): - """Dummy Exception.""" - - -class TestSamsungTv(unittest.TestCase): - """Testing Samsungtv component.""" - - @MockDependency("samsungctl") - @MockDependency("wakeonlan") - def setUp(self, samsung_mock, wol_mock): - """Set up test environment.""" - self.hass = tests.common.get_test_home_assistant() - self.hass.start() - self.hass.block_till_done() - self.device = SamsungTVDevice(**WORKING_CONFIG) - self.device._exceptions_class = mock.Mock() - self.device._exceptions_class.UnhandledResponse = UnhandledResponse - self.device._exceptions_class.AccessDenied = AccessDenied - self.device._exceptions_class.ConnectionClosed = ConnectionClosed - - def tearDown(self): - """Tear down test data.""" - self.hass.stop() - - @MockDependency("samsungctl") - @MockDependency("wakeonlan") - def test_setup(self, samsung_mock, wol_mock): - """Testing setup of platform.""" - with mock.patch("homeassistant.components.samsungtv.media_player.socket"): - add_entities = mock.Mock() - setup_platform(self.hass, WORKING_CONFIG, add_entities) - - @MockDependency("samsungctl") - @MockDependency("wakeonlan") - def test_setup_discovery(self, samsung_mock, wol_mock): - """Testing setup of platform with discovery.""" - with mock.patch("homeassistant.components.samsungtv.media_player.socket"): - add_entities = mock.Mock() - setup_platform(self.hass, {}, add_entities, discovery_info=DISCOVERY_INFO) - - @MockDependency("samsungctl") - @MockDependency("wakeonlan") - @mock.patch("homeassistant.components.samsungtv.media_player._LOGGER.warning") - def test_setup_none(self, samsung_mock, wol_mock, mocked_warn): - """Testing setup of platform with no data.""" - with mock.patch("homeassistant.components.samsungtv.media_player.socket"): - add_entities = mock.Mock() - setup_platform(self.hass, {}, add_entities, discovery_info=None) - mocked_warn.assert_called_once_with("Cannot determine device") - add_entities.assert_not_called() - - def test_update_on(self): - """Testing update tv on.""" - self.device.update() - self.assertEqual(STATE_ON, self.device._state) - - def test_update_off(self): - """Testing update tv off.""" - _remote = mock.Mock() - _remote.control = mock.Mock(side_effect=OSError("Boom")) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.update() - assert STATE_OFF == self.device._state - - def test_send_key(self): - """Test for send key.""" - self.device.send_key("KEY_POWER") - self.assertEqual(STATE_ON, self.device._state) - - def test_send_key_broken_pipe(self): - """Testing broken pipe Exception.""" - _remote = mock.Mock() - _remote.control = mock.Mock(side_effect=BrokenPipeError("Boom")) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.send_key("HELLO") - self.assertIsNone(self.device._remote) - self.assertEqual(STATE_ON, self.device._state) - - def test_send_key_connection_closed_retry_succeed(self): - """Test retry on connection closed.""" - _remote = mock.Mock() - _remote.control = mock.Mock( - side_effect=[ - self.device._exceptions_class.ConnectionClosed("Boom"), - mock.DEFAULT, - ] - ) - self.device.get_remote = mock.Mock(return_value=_remote) - command = "HELLO" - self.device.send_key(command) - self.assertEqual(STATE_ON, self.device._state) - # verify that _remote.control() get called twice because of retry logic - expected = [mock.call(command), mock.call(command)] - assert expected == _remote.control.call_args_list - - def test_send_key_unhandled_response(self): - """Testing unhandled response exception.""" - _remote = mock.Mock() - _remote.control = mock.Mock( - side_effect=self.device._exceptions_class.UnhandledResponse("Boom") - ) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.send_key("HELLO") - self.assertIsNone(self.device._remote) - self.assertEqual(STATE_ON, self.device._state) - - def test_send_key_os_error(self): - """Testing broken pipe Exception.""" - _remote = mock.Mock() - _remote.control = mock.Mock(side_effect=OSError("Boom")) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.send_key("HELLO") - assert self.device._remote is None - assert STATE_OFF == self.device._state - - def test_power_off_in_progress(self): - """Test for power_off_in_progress.""" - assert not self.device._power_off_in_progress() - self.device._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15) - assert self.device._power_off_in_progress() - - def test_name(self): - """Test for name property.""" - assert "fake" == self.device.name - - def test_state(self): - """Test for state property.""" - self.device._state = STATE_ON - self.assertEqual(STATE_ON, self.device.state) - self.device._state = STATE_OFF - assert STATE_OFF == self.device.state - - def test_is_volume_muted(self): - """Test for is_volume_muted property.""" - self.device._muted = False - assert not self.device.is_volume_muted - self.device._muted = True - assert self.device.is_volume_muted - - def test_supported_features(self): - """Test for supported_features property.""" - self.device._mac = None - assert SUPPORT_SAMSUNGTV == self.device.supported_features - self.device._mac = "fake" - assert SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON == self.device.supported_features - - def test_device_class(self): - """Test for device_class property.""" - assert DEVICE_CLASS_TV == self.device.device_class - - def test_turn_off(self): - """Test for turn_off.""" - self.device.send_key = mock.Mock() - _remote = mock.Mock() - _remote.close = mock.Mock() - self.get_remote = mock.Mock(return_value=_remote) - self.device._end_of_power_off = None - self.device.turn_off() - assert self.device._end_of_power_off is not None - self.device.send_key.assert_called_once_with("KEY_POWER") - self.device.send_key = mock.Mock() - self.device._config["method"] = "legacy" - self.device.turn_off() - self.device.send_key.assert_called_once_with("KEY_POWEROFF") - - @mock.patch("homeassistant.components.samsungtv.media_player._LOGGER.debug") - def test_turn_off_os_error(self, mocked_debug): - """Test for turn_off with OSError.""" - _remote = mock.Mock() - _remote.close = mock.Mock(side_effect=OSError("BOOM")) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.turn_off() - mocked_debug.assert_called_once_with("Could not establish connection.") - - def test_volume_up(self): - """Test for volume_up.""" - self.device.send_key = mock.Mock() - self.device.volume_up() - self.device.send_key.assert_called_once_with("KEY_VOLUP") - - def test_volume_down(self): - """Test for volume_down.""" - self.device.send_key = mock.Mock() - self.device.volume_down() - self.device.send_key.assert_called_once_with("KEY_VOLDOWN") - - def test_mute_volume(self): - """Test for mute_volume.""" - self.device.send_key = mock.Mock() - self.device.mute_volume(True) - self.device.send_key.assert_called_once_with("KEY_MUTE") - - def test_media_play_pause(self): - """Test for media_next_track.""" - self.device.send_key = mock.Mock() - self.device._playing = False - self.device.media_play_pause() - self.device.send_key.assert_called_once_with("KEY_PLAY") - assert self.device._playing - self.device.send_key = mock.Mock() - self.device.media_play_pause() - self.device.send_key.assert_called_once_with("KEY_PAUSE") - assert not self.device._playing - - def test_media_play(self): - """Test for media_play.""" - self.device.send_key = mock.Mock() - self.device._playing = False - self.device.media_play() - self.device.send_key.assert_called_once_with("KEY_PLAY") - assert self.device._playing - - def test_media_pause(self): - """Test for media_pause.""" - self.device.send_key = mock.Mock() - self.device._playing = True - self.device.media_pause() - self.device.send_key.assert_called_once_with("KEY_PAUSE") - assert not self.device._playing - - def test_media_next_track(self): - """Test for media_next_track.""" - self.device.send_key = mock.Mock() - self.device.media_next_track() - self.device.send_key.assert_called_once_with("KEY_FF") - - def test_media_previous_track(self): - """Test for media_previous_track.""" - self.device.send_key = mock.Mock() - self.device.media_previous_track() - self.device.send_key.assert_called_once_with("KEY_REWIND") - - def test_turn_on(self): - """Test turn on.""" - self.device.send_key = mock.Mock() - self.device._mac = None - self.device.turn_on() - self.device.send_key.assert_called_once_with("KEY_POWERON") - self.device._wol.send_magic_packet = mock.Mock() - self.device._mac = "fake" - self.device.turn_on() - self.device._wol.send_magic_packet.assert_called_once_with("fake") +@pytest.fixture(name="wakeonlan") +def wakeonlan_fixture(): + """Patch the wakeonlan Remote.""" + with MockDependency("wakeonlan") as wakeonlan: + yield wakeonlan @pytest.fixture -def samsung_mock(): - """Mock samsungctl.""" - with patch.dict("sys.modules", {"samsungctl": MagicMock()}): - yield +def mock_now(): + """Fixture for dtutil.now.""" + return dt_util.utcnow() -async def test_play_media(hass, samsung_mock): +async def setup_samsungtv(hass, config): + """Set up mock Samsung TV.""" + await async_setup_component(hass, "media_player", config) + await hass.async_block_till_done() + + +async def test_setup_with_mac(hass, remote): + """Test setup of platform.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert hass.states.get(ENTITY_ID) + + +async def test_setup_duplicate(hass, remote, caplog): + """Test duplicate setup of platform.""" + DUPLICATE = {DOMAIN: [MOCK_CONFIG[DOMAIN], MOCK_CONFIG[DOMAIN]]} + await setup_samsungtv(hass, DUPLICATE) + assert "Ignoring duplicate Samsung TV fake" in caplog.text + + +async def test_setup_without_mac(hass, remote): + """Test setup of platform.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + assert hass.states.get(ENTITY_ID_NOMAC) + + +async def test_setup_discovery(hass, remote): + """Test setup of platform with discovery.""" + hass.async_create_task( + async_load_platform( + hass, DOMAIN, SAMSUNGTV_DOMAIN, MOCK_CONFIG_DISCOVERY, {DOMAIN: {}} + ) + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID_DISCOVERY) + assert state + assert state.name == "fake_discovery (fake_model)" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get(ENTITY_ID_DISCOVERY) + assert entry + assert entry.unique_id == "fake_uuid" + + +async def test_setup_discovery_prefix(hass, remote): + """Test setup of platform with discovery.""" + hass.async_create_task( + async_load_platform( + hass, DOMAIN, SAMSUNGTV_DOMAIN, MOCK_CONFIG_DISCOVERY_PREFIX, {DOMAIN: {}} + ) + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID_DISCOVERY_PREFIX) + assert state + assert state.name == "fake_discovery_prefix (fake_model_prefix)" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get(ENTITY_ID_DISCOVERY_PREFIX) + assert entry + assert entry.unique_id == "fake_uuid_prefix" + + +async def test_update_on(hass, remote, mock_now): + """Testing update tv on.""" + await setup_samsungtv(hass, MOCK_CONFIG) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_update_off(hass, remote, mock_now): + """Testing update tv off.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=OSError("Boom")) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_send_key(hass, remote, wakeonlan): + """Test for send key.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_VOLUP"), call("KEY")] + assert state.state == STATE_ON + + +async def test_send_key_autodetect_websocket(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch("samsungctl.Remote") as remote, patch( + "homeassistant.components.samsungtv.media_player.socket" + ): + await setup_samsungtv(hass, MOCK_CONFIG_AUTO) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True + ) + state = hass.states.get(ENTITY_ID_AUTO) + assert remote.call_count == 1 + assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)] + assert state.state == STATE_ON + + +async def test_send_key_autodetect_websocket_exception(hass, caplog): + """Test for send key with autodetection of protocol.""" + with patch( + "samsungctl.Remote", side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT] + ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): + await setup_samsungtv(hass, MOCK_CONFIG_AUTO) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True + ) + state = hass.states.get(ENTITY_ID_AUTO) + # called 2 times because of the exception and the send key + assert remote.call_count == 2 + assert remote.call_args_list == [ + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_WEBSOCKET), + ] + assert state.state == STATE_ON + assert "Found working config without connection: " in caplog.text + assert "Failing config: " not in caplog.text + + +async def test_send_key_autodetect_legacy(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch( + "samsungctl.Remote", side_effect=[OSError("Boom"), mock.DEFAULT] + ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): + await setup_samsungtv(hass, MOCK_CONFIG_AUTO) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True + ) + state = hass.states.get(ENTITY_ID_AUTO) + assert remote.call_count == 2 + assert remote.call_args_list == [ + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_LEGACY), + ] + assert state.state == STATE_ON + + +async def test_send_key_autodetect_none(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch("samsungctl.Remote", side_effect=OSError("Boom")) as remote, patch( + "homeassistant.components.samsungtv.media_player.socket" + ): + await setup_samsungtv(hass, MOCK_CONFIG_AUTO) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True + ) + state = hass.states.get(ENTITY_ID_AUTO) + # 4 calls because of retry + assert remote.call_count == 4 + assert remote.call_args_list == [ + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_LEGACY), + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_LEGACY), + ] + assert state.state == STATE_UNKNOWN + + +async def test_send_key_broken_pipe(hass, remote): + """Testing broken pipe Exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=BrokenPipeError("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_send_key_connection_closed_retry_succeed(hass, remote): + """Test retry on connection closed.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock( + side_effect=[exceptions.ConnectionClosed("Boom"), mock.DEFAULT, mock.DEFAULT] + ) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + # key because of retry two times and update called + assert remote.control.call_count == 3 + assert remote.control.call_args_list == [ + call("KEY_VOLUP"), + call("KEY_VOLUP"), + call("KEY"), + ] + assert state.state == STATE_ON + + +async def test_send_key_unhandled_response(hass, remote): + """Testing unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=exceptions.UnhandledResponse("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_send_key_os_error(hass, remote): + """Testing broken pipe Exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=OSError("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_name(hass, remote): + """Test for name property.""" + await setup_samsungtv(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake" + + +async def test_state_with_mac(hass, remote, wakeonlan): + """Test for state property.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_state_without_mac(hass, remote): + """Test for state property.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + ) + state = hass.states.get(ENTITY_ID_NOMAC) + assert state.state == STATE_ON + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + ) + state = hass.states.get(ENTITY_ID_NOMAC) + assert state.state == STATE_OFF + + +async def test_supported_features_with_mac(hass, remote): + """Test for supported_features property.""" + await setup_samsungtv(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON + ) + + +async def test_supported_features_without_mac(hass, remote): + """Test for supported_features property.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + state = hass.states.get(ENTITY_ID_NOMAC) + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV + + +async def test_device_class(hass, remote): + """Test for device_class property.""" + await setup_samsungtv(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TV + + +async def test_turn_off_websocket(hass, remote): + """Test for turn_off.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_POWER")] + + +async def test_turn_off_legacy(hass, remote): + """Test for turn_off.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + ) + # key called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_POWEROFF")] + + +async def test_turn_off_os_error(hass, remote, caplog): + """Test for turn_off with OSError.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.close = mock.Mock(side_effect=OSError("BOOM")) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert "Could not establish connection." in caplog.text + + +async def test_volume_up(hass, remote): + """Test for volume_up.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_VOLUP"), call("KEY")] + + +async def test_volume_down(hass, remote): + """Test for volume_down.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_VOLDOWN"), call("KEY")] + + +async def test_mute_volume(hass, remote): + """Test for mute_volume.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: True}, + True, + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_MUTE"), call("KEY")] + + +async def test_media_play(hass, remote): + """Test for media_play.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_PLAY"), call("KEY")] + + +async def test_media_pause(hass, remote): + """Test for media_pause.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_PAUSE"), call("KEY")] + + +async def test_media_next_track(hass, remote): + """Test for media_next_track.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_FF"), call("KEY")] + + +async def test_media_previous_track(hass, remote): + """Test for media_previous_track.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_REWIND"), call("KEY")] + + +async def test_turn_on_with_mac(hass, remote, wakeonlan): + """Test turn on.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert wakeonlan.send_magic_packet.call_count == 1 + assert wakeonlan.send_magic_packet.call_args_list == [call("fake")] + + +async def test_turn_on_without_mac(hass, remote): + """Test turn on.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + ) + # nothing called as not supported feature + assert remote.control.call_count == 0 + + +async def test_play_media(hass, remote): """Test for play_media.""" asyncio_sleep = asyncio.sleep sleeps = [] @@ -312,57 +576,109 @@ async def test_play_media(hass, samsung_mock): sleeps.append(duration) await asyncio_sleep(0, loop=loop) + await setup_samsungtv(hass, MOCK_CONFIG) with patch("asyncio.sleep", new=sleep): - device = SamsungTVDevice(**WORKING_CONFIG) - device.hass = hass - - device.send_key = mock.Mock() - await device.async_play_media(MEDIA_TYPE_CHANNEL, "576") - - exp = [call("KEY_5"), call("KEY_7"), call("KEY_6"), call("KEY_ENTER")] - assert device.send_key.call_args_list == exp + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_ID: "576", + }, + True, + ) + # keys and update called + assert remote.control.call_count == 5 + assert remote.control.call_args_list == [ + call("KEY_5"), + call("KEY_7"), + call("KEY_6"), + call("KEY_ENTER"), + call("KEY"), + ] assert len(sleeps) == 3 -async def test_play_media_invalid_type(hass, samsung_mock): +async def test_play_media_invalid_type(hass, remote): """Test for play_media with invalid media type.""" url = "https://example.com" - device = SamsungTVDevice(**WORKING_CONFIG) - device.send_key = mock.Mock() - await device.async_play_media(MEDIA_TYPE_URL, url) - assert device.send_key.call_count == 0 + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_URL, + ATTR_MEDIA_CONTENT_ID: url, + }, + True, + ) + # only update called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY")] -async def test_play_media_channel_as_string(hass, samsung_mock): +async def test_play_media_channel_as_string(hass, remote): """Test for play_media with invalid channel as string.""" url = "https://example.com" - device = SamsungTVDevice(**WORKING_CONFIG) - device.send_key = mock.Mock() - await device.async_play_media(MEDIA_TYPE_CHANNEL, url) - assert device.send_key.call_count == 0 + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_ID: url, + }, + True, + ) + # only update called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY")] -async def test_play_media_channel_as_non_positive(hass, samsung_mock): +async def test_play_media_channel_as_non_positive(hass, remote): """Test for play_media with invalid channel as non positive integer.""" - device = SamsungTVDevice(**WORKING_CONFIG) - device.send_key = mock.Mock() - await device.async_play_media(MEDIA_TYPE_CHANNEL, "-4") - assert device.send_key.call_count == 0 + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_ID: "-4", + }, + True, + ) + # only update called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY")] -async def test_select_source(hass, samsung_mock): +async def test_select_source(hass, remote): """Test for select_source.""" - device = SamsungTVDevice(**WORKING_CONFIG) - device.hass = hass - device.send_key = mock.Mock() - await device.async_select_source("HDMI") - exp = [call("KEY_HDMI")] - assert device.send_key.call_args_list == exp + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "HDMI"}, + True, + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_HDMI"), call("KEY")] -async def test_select_source_invalid_source(hass, samsung_mock): +async def test_select_source_invalid_source(hass, remote): """Test for select_source with invalid source.""" - device = SamsungTVDevice(**WORKING_CONFIG) - device.send_key = mock.Mock() - await device.async_select_source("INVALID") - assert device.send_key.call_count == 0 + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "INVALID"}, + True, + ) + # only update called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY")] From 6bfb2460f2e4f6c93b23b07005fc9db5fb1cd0ef Mon Sep 17 00:00:00 2001 From: guillempages Date: Fri, 25 Oct 2019 16:06:52 +0200 Subject: [PATCH 023/306] [homematic]Pass channel to light color functions (#27306) The device HmIP-BSL has two independent LEDs on two different channels, so the channel needs to be explictly specified when setting the color. --- homeassistant/components/homematic/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 32fa0bb358e..29992bccef3 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -66,7 +66,7 @@ class HMLight(HMDevice, Light): """Return the hue and saturation color value [float, float].""" if not self.supported_features & SUPPORT_COLOR: return None - hue, sat = self._hmdevice.get_hs_color() + hue, sat = self._hmdevice.get_hs_color(self._channel) return hue * 360.0, sat * 100.0 @property @@ -98,6 +98,7 @@ class HMLight(HMDevice, Light): self._hmdevice.set_hs_color( hue=kwargs[ATTR_HS_COLOR][0] / 360.0, saturation=kwargs[ATTR_HS_COLOR][1] / 100.0, + channel=self._channel, ) if ATTR_EFFECT in kwargs: self._hmdevice.set_effect(kwargs[ATTR_EFFECT]) From 98cf3f4aa30cb0af02fffcb5561760d1a6dfc732 Mon Sep 17 00:00:00 2001 From: guillempages Date: Fri, 25 Oct 2019 16:08:11 +0200 Subject: [PATCH 024/306] [homematic]Add support for HmIP-BSL LEDs (#27307) With this commit, 3 entities are created for the HmIP-BSL device: 2 lights for the two independent LEDs and 1 switch for the relais --- homeassistant/components/homematic/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index cd791434f90..42fb73f6da2 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -82,6 +82,7 @@ HM_DEVICE_TYPES = { "IPKeySwitchPowermeter", "IPGarage", "IPKeySwitch", + "IPKeySwitchLevel", "IPMultiIO", ], DISCOVER_LIGHTS: [ @@ -90,6 +91,7 @@ HM_DEVICE_TYPES = { "IPKeyDimmer", "IPDimmer", "ColorEffectLight", + "IPKeySwitchLevel", ], DISCOVER_SENSORS: [ "SwitchPowermeter", @@ -671,6 +673,11 @@ def _get_devices(hass, discovery_type, keys, interface): and class_name not in HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []) ): continue + if discovery_type == DISCOVER_SWITCHES and class_name == "IPKeySwitchLevel": + channels.remove(8) + channels.remove(12) + if discovery_type == DISCOVER_LIGHTS and class_name == "IPKeySwitchLevel": + channels.remove(4) # Add devices _LOGGER.debug( From 9153729b2105ffac4738df13a1ac7428eb83cfce Mon Sep 17 00:00:00 2001 From: gngj Date: Fri, 25 Oct 2019 19:02:40 +0300 Subject: [PATCH 025/306] move hass-frontend import back down (#28203) --- homeassistant/components/frontend/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 541d1bf473d..f82350d994d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -7,7 +7,6 @@ import pathlib from typing import Any, Dict, Optional, Set, Tuple from aiohttp import hdrs, web, web_urldispatcher -import hass_frontend import jinja2 import voluptuous as vol from yarl import URL @@ -241,6 +240,8 @@ def _frontend_root(dev_repo_path): """Return root path to the frontend files.""" if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" + # Keep import here so that we can import frontend without installing reqs + import hass_frontend return hass_frontend.where() From 43c7b57d1ec2dc45cc8dff09147c205e082389ed Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 25 Oct 2019 11:37:50 -0500 Subject: [PATCH 026/306] Update Plex via websockets (#28158) * Save client identifier from auth for future use * Use websocket events to update Plex * Handle websocket disconnections * Use aiohttp, shut down socket cleanly * Bad rebase fix * Don't connect websocket during config_flow validation, fix tests * Move websocket handling to external library * Close websocket session on HA stop * Use external library, revert unnecessary test change * Async & lint fixes * Clean up websocket stopper on entry unload * Setup websocket in component, pass actual needed object to library --- .coveragerc | 1 + homeassistant/components/plex/__init__.py | 45 ++++++++++++++++----- homeassistant/components/plex/const.py | 3 +- homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/server.py | 7 ++++ requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ 7 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3350bc359af..90efc417e03 100644 --- a/.coveragerc +++ b/.coveragerc @@ -514,6 +514,7 @@ omit = homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py homeassistant/components/plex/server.py + homeassistant/components/plex/websockets.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index b6ed3245115..1aaa8a8e3aa 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,9 +1,9 @@ """Support to embed Plex.""" import asyncio -from datetime import timedelta import logging import plexapi.exceptions +from plexwebsocket import PlexWebsocket import requests.exceptions import voluptuous as vol @@ -16,9 +16,14 @@ from homeassistant.const import ( CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ( CONF_USE_EPISODE_ART, @@ -33,8 +38,9 @@ from .const import ( PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, - REFRESH_LISTENERS, + PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, + WEBSOCKETS, ) from .server import PlexServer @@ -67,9 +73,7 @@ _LOGGER = logging.getLogger(__package__) def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault( - PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}, DISPATCHERS: {}} - ) + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -136,7 +140,6 @@ async def async_setup_entry(hass, entry): ) server_id = plex_server.machine_identifier hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id] = [] for platform in PLATFORMS: hass.async_create_task( @@ -145,9 +148,29 @@ async def async_setup_entry(hass, entry): entry.add_update_listener(async_options_updated) - hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = async_track_time_interval( - hass, lambda now: plex_server.update_platforms(), timedelta(seconds=10) + unsub = async_dispatcher_connect( + hass, + PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), + plex_server.update_platforms, ) + hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + def update_plex(): + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + + session = async_get_clientsession(hass) + websocket = PlexWebsocket(plex_server.plex_server, update_plex, session) + hass.loop.create_task(websocket.listen()) + hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + + def close_websocket_session(_): + websocket.close() + + unsub = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, close_websocket_session + ) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) return True @@ -156,8 +179,8 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] - cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) - cancel() + websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id) + websocket.close() dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) for unsub in dispatchers: diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 0d512101e11..d3c79e60bc4 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -10,8 +10,8 @@ DEFAULT_VERIFY_SSL = True DISPATCHERS = "dispatchers" PLATFORMS = ["media_player", "sensor"] -REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" +WEBSOCKETS = "websockets" PLEX_CONFIG_FILE = "plex.conf" PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" @@ -19,6 +19,7 @@ PLEX_SERVER_CONFIG = "server_config" PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" +PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" CONF_CLIENT_IDENTIFIER = "client_id" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 3c570a0e64c..90ae305148e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", - "plexauth==0.0.5" + "plexauth==0.0.5", + "plexwebsocket==0.0.1" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index c0461ee0f54..e6f77a310f1 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -103,6 +103,8 @@ class PlexServer: def update_platforms(self): """Update the platform entities.""" + _LOGGER.debug("Updating devices") + available_clients = {} new_clients = set() @@ -164,6 +166,11 @@ class PlexServer: sessions, ) + @property + def plex_server(self): + """Return the plexapi PlexServer instance.""" + return self._plex_server + @property def friendly_name(self): """Return name of connected Plex server.""" diff --git a/requirements_all.txt b/requirements_all.txt index 81009711ed7..32115995df1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -973,6 +973,9 @@ plexapi==3.0.6 # homeassistant.components.plex plexauth==0.0.5 +# homeassistant.components.plex +plexwebsocket==0.0.1 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25ca8abdbf3..0b8cfb1f57f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,6 +346,9 @@ plexapi==3.0.6 # homeassistant.components.plex plexauth==0.0.5 +# homeassistant.components.plex +plexwebsocket==0.0.1 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 From 0656f0c62b980d6f47af8b47eace45524e848acd Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 25 Oct 2019 19:39:16 +0300 Subject: [PATCH 027/306] Address post-merge coolmaster config flow code review (#28163) * Address post-merge code review comments * Use component path for 3rd party lib --- homeassistant/components/coolmaster/__init__.py | 10 ++++------ homeassistant/components/coolmaster/config_flow.py | 7 +++---- tests/components/coolmaster/test_config_flow.py | 14 ++++++-------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 530427d33ad..c666c39cfb3 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -8,15 +8,13 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Coolmaster from a config entry.""" - hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, "climate")) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) return True async def async_unload_entry(hass, entry): """Unload a Coolmaster config entry.""" - await hass.async_add_job( - hass.config_entries.async_forward_entry_unload(entry, "climate") - ) - - return True + return await hass.config_entries.async_forward_entry_unload(entry, "climate") diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index 543b4c239c8..fe52ea17b28 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -14,11 +14,10 @@ MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MOD DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) -async def validate_connection(hass: core.HomeAssistant, host): - """Validate that we can connect to the Coolmaster instance.""" +async def _validate_connection(hass: core.HomeAssistant, host): cool = CoolMasterNet(host, port=DEFAULT_PORT) devices = await hass.async_add_executor_job(cool.devices) - return len(devices) > 0 + return bool(devices) class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -50,7 +49,7 @@ class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host = user_input[CONF_HOST] try: - result = await validate_connection(self.hass, host) + result = await _validate_connection(self.hass, host) if not result: errors["base"] = "no_units" except (ConnectionRefusedError, TimeoutError): diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index d49858fcf05..d0126ff2cb6 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -4,8 +4,6 @@ from unittest.mock import patch from homeassistant import config_entries, setup from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES -# from homeassistant.components.coolmaster.config_flow import validate_connection - from tests.common import mock_coro @@ -26,8 +24,8 @@ async def test_form(hass): assert result["errors"] is None with patch( - "homeassistant.components.coolmaster.config_flow.validate_connection", - return_value=mock_coro(True), + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", + return_value=[1], ), patch( "homeassistant.components.coolmaster.async_setup", return_value=mock_coro(True) ) as mock_setup, patch( @@ -57,7 +55,7 @@ async def test_form_timeout(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.validate_connection", + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", side_effect=TimeoutError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -75,7 +73,7 @@ async def test_form_connection_refused(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.validate_connection", + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", side_effect=ConnectionRefusedError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -93,8 +91,8 @@ async def test_form_no_units(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.validate_connection", - return_value=mock_coro(False), + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", + return_value=[], ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], _flow_data() From 3c4caaaefc4db53fe05c5bcdf6c46871499863e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 25 Oct 2019 20:09:18 +0300 Subject: [PATCH 028/306] Add presentation URL to SSDP discovery info (#28196) --- homeassistant/components/ssdp/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index a8591ac042b..d2382748f30 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -26,6 +26,7 @@ ATTR_MANUFACTURER = "manufacturer" ATTR_MANUFACTURERURL = "manufacturerURL" ATTR_UDN = "udn" ATTR_UPNP_DEVICE_TYPE = "upnp_device_type" +ATTR_PRESENTATIONURL = "presentation_url" _LOGGER = logging.getLogger(__name__) @@ -175,5 +176,6 @@ def info_from_entry(entry, device_info): info[ATTR_MANUFACTURERURL] = device_info.get("manufacturerURL") info[ATTR_UDN] = device_info.get("UDN") info[ATTR_UPNP_DEVICE_TYPE] = device_info.get("deviceType") + info[ATTR_PRESENTATIONURL] = device_info.get("presentationURL") return info From 5c8a9c2815b567c94033ba1ef19ed016faf8bbc0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 25 Oct 2019 19:20:42 +0200 Subject: [PATCH 029/306] Updated frontend to 20191025.0 (#28208) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ae53b972dca..b23d40605dd 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191023.0" + "home-assistant-frontend==20191025.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0e06690c856..fbfa1dbf67b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 32115995df1..ff633960f82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b8cfb1f57f..8f55cb1aa1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 38428308fccdc3c042aa599fca80b0c1d83a8857 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 13:21:22 -0400 Subject: [PATCH 030/306] Change Alexa default display category based on media_player device_class (#28191) * Support default display category based one media_player device_class. * Support default display category based one media_player device_class. --- homeassistant/components/alexa/entities.py | 4 ++++ tests/components/alexa/test_smart_home.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index d84848e9aba..e52e1b4f87e 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -391,6 +391,10 @@ class MediaPlayerCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class == media_player.DEVICE_CLASS_SPEAKER: + return [DisplayCategory.SPEAKER] + return [DisplayCategory.TV] def interfaces(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index c50c0748147..1de7d404ef6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -968,6 +968,25 @@ async def test_media_player_power(hass): ) +async def test_media_player_speaker(hass): + """Test media player discovery with device class speaker.""" + device = ( + "media_player.test", + "off", + { + "friendly_name": "Test media player", + "supported_features": 51765, + "volume_level": 0.75, + "device_class": "speaker", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["displayCategories"][0] == "SPEAKER" + assert appliance["friendlyName"] == "Test media player" + + async def test_alert(hass): """Test alert discovery.""" device = ("alert.test", "off", {"friendly_name": "Test alert"}) From 2c914e0c59453d73fa3387a6b2b73a4485bbb898 Mon Sep 17 00:00:00 2001 From: Hayley McIldoon Date: Fri, 25 Oct 2019 13:22:39 -0400 Subject: [PATCH 031/306] Add device condition support for media_player (#28161) * Add device condition for media_player * Fix typo in strings --- .../media_player/device_condition.py | 117 +++++++++ .../components/media_player/strings.json | 11 + .../media_player/test_device_condition.py | 243 ++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 homeassistant/components/media_player/device_condition.py create mode 100644 homeassistant/components/media_player/strings.json create mode 100644 tests/components/media_player/test_device_condition.py diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py new file mode 100644 index 00000000000..a67a084a94f --- /dev/null +++ b/homeassistant/components/media_player/device_condition.py @@ -0,0 +1,117 @@ +"""Provides device automations for Media player.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_on", "is_off", "is_idle", "is_paused", "is_playing"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Media player devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_on", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_off", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_idle", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_paused", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_playing", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_playing": + state = STATE_PLAYING + elif config[CONF_TYPE] == "is_idle": + state = STATE_IDLE + elif config[CONF_TYPE] == "is_paused": + state = STATE_PAUSED + elif config[CONF_TYPE] == "is_on": + state = STATE_ON + else: + state = STATE_OFF + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json new file mode 100644 index 00000000000..e9cb812767b --- /dev/null +++ b/homeassistant/components/media_player/strings.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off", + "is_idle": "{entity_name} is idle", + "is_paused": "{entity_name} is paused", + "is_playing": "{entity_name} is playing" + } + } +} diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py new file mode 100644 index 00000000000..6216cc0e2b0 --- /dev/null +++ b/tests/components/media_player/test_device_condition.py @@ -0,0 +1,243 @@ +"""The tests for Media player device conditions.""" +import pytest + +from homeassistant.components.media_player import DOMAIN +from homeassistant.const import ( + STATE_ON, + STATE_OFF, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a media_player.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_idle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_paused", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_playing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("media_player.entity", STATE_ON) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_idle", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_idle - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event4"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_paused", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_paused - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event5"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_playing", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_playing - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on - event - test_event1" + + hass.states.async_set("media_player.entity", STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off - event - test_event2" + + hass.states.async_set("media_player.entity", STATE_IDLE) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_idle - event - test_event3" + + hass.states.async_set("media_player.entity", STATE_PAUSED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_paused - event - test_event4" + + hass.states.async_set("media_player.entity", STATE_PLAYING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data["some"] == "is_playing - event - test_event5" From d28f7ab120ce8a4eabb6b54513e8c441687c99c7 Mon Sep 17 00:00:00 2001 From: gngj Date: Fri, 25 Oct 2019 20:42:23 +0300 Subject: [PATCH 032/306] Fix microsoft tts (#28199) * Update pycsspeechtts From 1.0.2 to 1.0.3 as the old one is using an api that doesn't work * Give a option to choose region Api is now region dependent, so gave it a config --- homeassistant/components/microsoft/manifest.json | 2 +- homeassistant/components/microsoft/tts.py | 11 +++++++++-- requirements_all.txt | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 16ae94c212e..5834897ee90 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -3,7 +3,7 @@ "name": "Microsoft", "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": [ - "pycsspeechtts==1.0.2" + "pycsspeechtts==1.0.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 3536c788bb9..d214f6648dd 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -14,6 +14,7 @@ CONF_RATE = "rate" CONF_VOLUME = "volume" CONF_PITCH = "pitch" CONF_CONTOUR = "contour" +CONF_REGION = "region" _LOGGER = logging.getLogger(__name__) @@ -72,6 +73,7 @@ DEFAULT_RATE = 0 DEFAULT_VOLUME = 0 DEFAULT_PITCH = "default" DEFAULT_CONTOUR = "" +DEFAULT_REGION = "eastus" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -87,6 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ), vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): cv.string, vol.Optional(CONF_CONTOUR, default=DEFAULT_CONTOUR): cv.string, + vol.Optional(CONF_REGION, default=DEFAULT_REGION): cv.string, } ) @@ -102,13 +105,16 @@ def get_engine(hass, config): config[CONF_VOLUME], config[CONF_PITCH], config[CONF_CONTOUR], + config[CONF_REGION], ) class MicrosoftProvider(Provider): """The Microsoft speech API provider.""" - def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour): + def __init__( + self, apikey, lang, gender, ttype, rate, volume, pitch, contour, region + ): """Init Microsoft TTS service.""" self._apikey = apikey self._lang = lang @@ -119,6 +125,7 @@ class MicrosoftProvider(Provider): self._volume = f"{volume}%" self._pitch = pitch self._contour = contour + self._region = region self.name = "Microsoft" @property @@ -138,7 +145,7 @@ class MicrosoftProvider(Provider): from pycsspeechtts import pycsspeechtts try: - trans = pycsspeechtts.TTSTranslator(self._apikey) + trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) data = trans.speak( language=language, gender=self._gender, diff --git a/requirements_all.txt b/requirements_all.txt index ff633960f82..d3df0a3ab98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1132,7 +1132,7 @@ pycomfoconnect==0.3 pycoolmasternet==0.0.4 # homeassistant.components.microsoft -pycsspeechtts==1.0.2 +pycsspeechtts==1.0.3 # homeassistant.components.cups # pycups==1.9.73 From 7ddce1d52b7a967eb0de63f331bdcfd9afe935bb Mon Sep 17 00:00:00 2001 From: Hayley McIldoon Date: Fri, 25 Oct 2019 13:51:35 -0400 Subject: [PATCH 033/306] Add device condition support for device_tracker (#28190) --- .../device_tracker/device_condition.py | 81 +++++++++++ .../components/device_tracker/strings.json | 8 ++ .../device_tracker/test_device_condition.py | 126 ++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 homeassistant/components/device_tracker/device_condition.py create mode 100644 homeassistant/components/device_tracker/strings.json create mode 100644 tests/components/device_tracker/test_device_condition.py diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py new file mode 100644 index 00000000000..6379aca6c0b --- /dev/null +++ b/homeassistant/components/device_tracker/device_condition.py @@ -0,0 +1,81 @@ +"""Provides device automations for Device tracker.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_NOT_HOME, + STATE_HOME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_home", "is_not_home"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Device tracker devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_home", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_not_home", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_home": + state = STATE_HOME + else: + state = STATE_NOT_HOME + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json new file mode 100644 index 00000000000..7e0691654a0 --- /dev/null +++ b/homeassistant/components/device_tracker/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} is home", + "is_not_home": "{entity_name} is not home" + } + } +} \ No newline at end of file diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py new file mode 100644 index 00000000000..732abccd8ca --- /dev/null +++ b/tests/components/device_tracker/test_device_condition.py @@ -0,0 +1,126 @@ +"""The tests for Device tracker device conditions.""" +import pytest + +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import STATE_HOME, STATE_NOT_HOME +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a device_tracker.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_not_home", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_home", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("device_tracker.entity", STATE_HOME) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "device_tracker.entity", + "type": "is_home", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "device_tracker.entity", + "type": "is_not_home", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_not_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_home - event - test_event1" + + hass.states.async_set("device_tracker.entity", STATE_NOT_HOME) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_not_home - event - test_event2" From f2d6cc732906d51eccb17350a0b5fd15fb3d080e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20P=C3=B6schl?= Date: Fri, 25 Oct 2019 21:25:27 +0200 Subject: [PATCH 034/306] Increased python-eq3bt version to latest (0.1.11) (#28175) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 1065b94c12a..e168752d83d 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": [ "construct==2.9.45", - "python-eq3bt==0.1.9" + "python-eq3bt==0.1.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d3df0a3ab98..8e8a72d0181 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1504,7 +1504,7 @@ python-digitalocean==1.13.2 python-ecobee-api==0.1.4 # homeassistant.components.eq3btsmart -# python-eq3bt==0.1.9 +# python-eq3bt==0.1.11 # homeassistant.components.etherscan python-etherscan-api==0.0.3 From da8a47614282129428e386253ad233f7071d5ced Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 16:34:52 -0400 Subject: [PATCH 035/306] Add support for supportedOperations to Alexa.PlaybackController (#28212) * Added support for supportedOperations to Alexa.PlaybackController * Added support for supportedOperations to Alexa.PlaybackController --- .../components/alexa/capabilities.py | 32 +++++++++++++++++++ tests/components/alexa/test_smart_home.py | 9 +++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index deb83813dbc..56a18f1d521 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) import homeassistant.components.climate.const as climate +import homeassistant.components.media_player.const as media_player from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util @@ -110,6 +111,11 @@ class AlexaCapability: """Return the Configuration object.""" return [] + @staticmethod + def supported_operations(): + """Return the supportedOperations object.""" + return [] + def serialize_discovery(self): """Serialize according to the Discovery API.""" result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} @@ -150,6 +156,10 @@ class AlexaCapability: if instance is not None: result["instance"] = instance + supported_operations = self.supported_operations() + if supported_operations: + result["supportedOperations"] = supported_operations + return result def serialize_properties(self): @@ -484,6 +494,28 @@ class AlexaPlaybackController(AlexaCapability): """Return the Alexa API name of this interface.""" return "Alexa.PlaybackController" + def supported_operations(self): + """Return the supportedOperations object. + + Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, StartOver, Stop + """ + supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + operations = { + media_player.SUPPORT_NEXT_TRACK: "Next", + media_player.SUPPORT_PAUSE: "Pause", + media_player.SUPPORT_PLAY: "Play", + media_player.SUPPORT_PREVIOUS_TRACK: "Previous", + media_player.SUPPORT_STOP: "Stop", + } + + supported_operations = [] + for operation in operations: + if operation & supported_features: + supported_operations.append(operations[operation]) + + return supported_operations + class AlexaInputController(AlexaCapability): """Implements Alexa.InputController. diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1de7d404ef6..177d52e83de 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -726,7 +726,7 @@ async def test_media_player(hass): assert appliance["displayCategories"][0] == "TV" assert appliance["friendlyName"] == "Test media player" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.InputController", "Alexa.PowerController", @@ -737,6 +737,13 @@ async def test_media_player(hass): "Alexa.ChannelController", ) + playback_capability = get_capability(capabilities, "Alexa.PlaybackController") + assert playback_capability is not None + supported_operations = playback_capability["supportedOperations"] + operations = ["Play", "Pause", "Stop", "Next", "Previous"] + for operation in operations: + assert operation in supported_operations + await assert_power_controller_works( "media_player#test", "media_player.turn_on", "media_player.turn_off", hass ) From f4341c1546eccd1a0999792a85a2e0f58ba9f726 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 04:40:05 +0800 Subject: [PATCH 036/306] Fix broken deconz trigger (#28211) --- homeassistant/components/deconz/device_trigger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 27ff6fcd590..2d097d30c0b 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -235,6 +235,7 @@ async def async_attach_trigger(hass, config, action, automation_info): event_id = deconz_event.serial event_config = { + event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } From d44bfa8e8874f6b3c6727998650884f62c8621c7 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 16:42:21 -0400 Subject: [PATCH 037/306] Improved Alexa ThermostatController thermostatMode handling (#28176) * Update ThermostatController to map directives to supported modes and add support for CUSTOM mode. * Removed erroneous config value from test. * Removed unnecessary use of a comprehension by dumbing down comment so pylint could comprehend. * Removed erroneous import variable caused by removing erroneous config value from test. * Removed unnecessary use of a comprehension. * Reverted Removal or erroneous import variable and erroneous config value from test. Apparently need for additional tests outside this component. Whoops. --- .../components/alexa/capabilities.py | 27 ++++++++++++ homeassistant/components/alexa/const.py | 3 +- homeassistant/components/alexa/handlers.py | 22 +++++++++- tests/components/alexa/test_capabilities.py | 23 ++++++++--- tests/components/alexa/test_smart_home.py | 41 +++++++++++++++++-- 5 files changed, 105 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 56a18f1d521..52dde74ff3a 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -736,6 +736,33 @@ class AlexaThermostatController(AlexaCapability): return {"value": temp, "scale": API_TEMP_UNITS[unit]} + def configuration(self): + """Return configuration object. + + Translates climate HVAC_MODES and PRESETS to supported Alexa ThermostatMode Values. + ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM. + """ + supported_modes = [] + hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) + for mode in hvac_modes: + thermostat_mode = API_THERMOSTAT_MODES.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) + + preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) + for mode in preset_modes: + thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) + + # Return False for supportsScheduling until supported with event listener in handler. + configuration = {"supportsScheduling": False} + + if supported_modes: + configuration["supportedModes"] = supported_modes + + return configuration + class AlexaPowerLevelController(AlexaCapability): """Implements Alexa.PowerLevelController. diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 8d1f0ac95a5..2a5f9a512b3 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -56,9 +56,10 @@ API_THERMOSTAT_MODES = OrderedDict( (climate.HVAC_MODE_AUTO, "AUTO"), (climate.HVAC_MODE_OFF, "OFF"), (climate.HVAC_MODE_FAN_ONLY, "OFF"), - (climate.HVAC_MODE_DRY, "OFF"), + (climate.HVAC_MODE_DRY, "CUSTOM"), ] ) +API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} PERCENTAGE_FAN_MAP = { diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 331990dc4a4..3dadf51509a 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -38,6 +38,7 @@ from homeassistant.util.temperature import convert as convert_temperature from .const import ( API_TEMP_UNITS, + API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause, @@ -767,11 +768,28 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_PRESET_MODE - data[climate.ATTR_PRESET_MODE] = climate.PRESET_ECO + data[climate.ATTR_PRESET_MODE] = ha_preset + + elif mode == "CUSTOM": + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) + custom_mode = directive.payload["thermostatMode"]["customName"] + custom_mode = next( + (k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), + None, + ) + if custom_mode not in operation_list: + msg = ( + f"The requested thermostat mode {mode}: {custom_mode} is not supported" + ) + raise AlexaUnsupportedThermostatModeError(msg) + + service = climate.SERVICE_SET_HVAC_MODE + data[climate.ATTR_HVAC_MODE] = custom_mode else: operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) - ha_mode = next((k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None) + ha_modes = {k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode} + ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None) if ha_mode not in operation_list: msg = f"The requested thermostat mode {mode} is not supported" raise AlexaUnsupportedThermostatModeError(msg) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index be4a2ba4806..89815c72544 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -472,11 +472,7 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in ( - climate.HVAC_MODE_OFF, - climate.HVAC_MODE_FAN_ONLY, - climate.HVAC_MODE_DRY, - ): + for off_modes in (climate.HVAC_MODE_OFF, climate.HVAC_MODE_FAN_ONLY): hass.states.async_set( "climate.downstairs", off_modes, @@ -495,6 +491,23 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) + # assert dry is reported as CUSTOM + hass.states.async_set( + "climate.downstairs", + "dry", + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + hass.states.async_set( "climate.heat", "heat", diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 177d52e83de..00c762103f3 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1267,7 +1267,7 @@ async def test_thermostat(hass): "current_temperature": 75.0, "friendly_name": "Test Thermostat", "supported_features": 1 | 2 | 4 | 128, - "hvac_modes": ["heat", "cool", "auto", "off"], + "hvac_modes": ["off", "heat", "cool", "auto", "dry"], "preset_mode": None, "preset_modes": ["eco"], "min_temp": 50, @@ -1280,7 +1280,7 @@ async def test_thermostat(hass): assert appliance["displayCategories"][0] == "THERMOSTAT" assert appliance["friendlyName"] == "Test Thermostat" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.ThermostatController", @@ -1299,6 +1299,15 @@ async def test_thermostat(hass): "Alexa.TemperatureSensor", "temperature", {"value": 75.0, "scale": "FAHRENHEIT"} ) + thermostat_capability = get_capability(capabilities, "Alexa.ThermostatController") + assert thermostat_capability is not None + configuration = thermostat_capability["configuration"] + assert configuration["supportsScheduling"] is False + + supported_modes = ["OFF", "HEAT", "COOL", "AUTO", "ECO", "CUSTOM"] + for mode in supported_modes: + assert mode in configuration["supportedModes"] + call, msg = await assert_request_calls_service( "Alexa.ThermostatController", "SetTargetTemperature", @@ -1447,6 +1456,30 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") + # Assert we can call custom modes + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "DEHUMIDIFY"}}, + ) + assert call.data["hvac_mode"] == "dry" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + + # assert unsupported custom mode + msg = await assert_request_fails( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "INVALID"}}, + ) + assert msg["event"]["payload"]["type"] == "UNSUPPORTED_THERMOSTAT_MODE" + msg = await assert_request_fails( "Alexa.ThermostatController", "SetThermostatMode", @@ -1456,7 +1489,6 @@ async def test_thermostat(hass): payload={"thermostatMode": {"value": "INVALID"}}, ) assert msg["event"]["payload"]["type"] == "UNSUPPORTED_THERMOSTAT_MODE" - hass.config.units.temperature_unit = TEMP_CELSIUS call, _ = await assert_request_calls_service( "Alexa.ThermostatController", @@ -1479,6 +1511,9 @@ async def test_thermostat(hass): ) assert call.data["preset_mode"] == "eco" + # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. + hass.config.units.temperature_unit = TEMP_CELSIUS + async def test_exclude_filters(hass): """Test exclusion filters.""" From 7fee44b8c598e5e2b5e5950f5a3bf1078d36cd94 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 04:50:46 +0800 Subject: [PATCH 038/306] Add additional device conditions to cover (#27830) * Add additional device conditions to cover * Add default value * Add test * Use numeric_state condition instead of template condition --- .../components/cover/device_condition.py | 208 ++++++-- homeassistant/components/cover/strings.json | 4 +- .../components/cover/test_device_condition.py | 476 +++++++++++++++++- .../custom_components/test/cover.py | 61 +++ 4 files changed, 689 insertions(+), 60 deletions(-) create mode 100644 tests/testing_config/custom_components/test/cover.py diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 129462047e4..487f815afb5 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -4,6 +4,9 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ABOVE, + CONF_BELOW, CONF_CONDITION, CONF_DOMAIN, CONF_TYPE, @@ -15,20 +18,50 @@ from homeassistant.const import ( STATE_CLOSING, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry, + template, +) from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA -from . import DOMAIN +from . import ( + DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, +) -CONDITION_TYPES = {"is_open", "is_closed", "is_opening", "is_closing"} +POSITION_CONDITION_TYPES = {"is_position", "is_tilt_position"} +STATE_CONDITION_TYPES = {"is_open", "is_closed", "is_opening", "is_closing"} -CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( +POSITION_CONDITION_SCHEMA = vol.All( + DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(POSITION_CONDITION_TYPES), + vol.Optional(CONF_ABOVE): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(CONF_BELOW): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + +STATE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + vol.Required(CONF_TYPE): vol.In(STATE_CONDITION_TYPES), } ) +CONDITION_SCHEMA = vol.Any(POSITION_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA) + async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device conditions for Cover devices.""" @@ -40,64 +73,133 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict if entry.domain != DOMAIN: continue + state = hass.states.get(entry.entity_id) + if not state or ATTR_SUPPORTED_FEATURES not in state.attributes: + continue + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE) + # Add conditions for each entity that belongs to this integration - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_open", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_closed", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_opening", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_closing", - } - ) + if supports_open_close: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_open", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closed", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_opening", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closing", + } + ) + if supported_features & SUPPORT_SET_POSITION: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_position", + } + ) + if supported_features & SUPPORT_SET_TILT_POSITION: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_tilt_position", + } + ) return conditions +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + if config[CONF_TYPE] not in ["is_position", "is_tilt_position"]: + return {} + + return { + "extra_fields": vol.Schema( + { + vol.Optional(CONF_ABOVE, default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(CONF_BELOW, default=100): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + } + ) + } + + def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config_validation: config = CONDITION_SCHEMA(config) - if config[CONF_TYPE] == "is_open": - state = STATE_OPEN - elif config[CONF_TYPE] == "is_closed": - state = STATE_CLOSED - elif config[CONF_TYPE] == "is_opening": - state = STATE_OPENING - elif config[CONF_TYPE] == "is_closing": - state = STATE_CLOSING - def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: - """Test if an entity is a certain state.""" - return condition.state(hass, config[ATTR_ENTITY_ID], state) + if config[CONF_TYPE] in STATE_CONDITION_TYPES: + if config[CONF_TYPE] == "is_open": + state = STATE_OPEN + elif config[CONF_TYPE] == "is_closed": + state = STATE_CLOSED + elif config[CONF_TYPE] == "is_opening": + state = STATE_OPENING + elif config[CONF_TYPE] == "is_closing": + state = STATE_CLOSING - return test_is_state + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state + + if config[CONF_TYPE] == "is_position": + position = "current_position" + if config[CONF_TYPE] == "is_tilt_position": + position = "current_tilt_position" + min_pos = config.get(CONF_ABOVE, None) + max_pos = config.get(CONF_BELOW, None) + value_template = template.Template( # type: ignore + f"{{{{ state.attributes.{position} }}}}" + ) + + def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: + """Validate template based if-condition.""" + value_template.hass = hass + + return condition.async_numeric_state( + hass, config[ATTR_ENTITY_ID], max_pos, min_pos, value_template + ) + + return template_if diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index db3ccf9119f..e4c72746ee4 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -4,7 +4,9 @@ "is_open": "{entity_name} is open", "is_closed": "{entity_name} is closed", "is_opening": "{entity_name} is opening", - "is_closing": "{entity_name} is closing" + "is_closing": "{entity_name} is closing", + "is_position": "{entity_name} position is", + "is_tilt_position": "{entity_name} tilt position is" } } } \ No newline at end of file diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 494368f76ff..8ca912b640b 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -2,7 +2,13 @@ import pytest from homeassistant.components.cover import DOMAIN -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING +from homeassistant.const import ( + CONF_PLATFORM, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry @@ -14,6 +20,7 @@ from tests.common import ( mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -37,47 +44,298 @@ def calls(hass): async def test_get_conditions(hass, device_reg, entity_reg): """Test we get the expected conditions from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[0] + config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + expected_conditions = [ { "condition": "device", "domain": DOMAIN, "type": "is_open", "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", }, { "condition": "device", "domain": DOMAIN, "type": "is_closed", "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", }, { "condition": "device", "domain": DOMAIN, "type": "is_opening", "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", }, { "condition": "device", "domain": DOMAIN, "type": "is_closing", "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", }, ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) assert_lists_same(conditions, expected_conditions) +async def test_get_conditions_set_pos(hass, device_reg, entity_reg): + """Test we get the expected conditions from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_open", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_position", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_get_conditions_set_tilt_pos(hass, device_reg, entity_reg): + """Test we get the expected conditions from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_open", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_tilt_position", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[0] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 4 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == {"extra_fields": []} + + +async def test_get_condition_capabilities_set_pos(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "name": "above", + "optional": True, + "type": "integer", + "default": 0, + "valueMax": 100, + "valueMin": 0, + }, + { + "name": "below", + "optional": True, + "type": "integer", + "default": 100, + "valueMax": 100, + "valueMin": 0, + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 5 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + if condition["type"] == "is_position": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + +async def test_get_condition_capabilities_set_tilt_pos(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "name": "above", + "optional": True, + "type": "integer", + "default": 0, + "valueMax": 100, + "valueMin": 0, + }, + { + "name": "below", + "optional": True, + "type": "integer", + "default": 100, + "valueMax": 100, + "valueMin": 0, + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 5 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + if condition["type"] == "is_tilt_position": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" hass.states.async_set("cover.entity", STATE_OPEN) @@ -188,3 +446,209 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 4 assert calls[3].data["some"] == "is_closing - event - test_event4" + + +async def test_if_position(hass, calls): + """Test for position conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_position", + "above": 45, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_position", + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_position", + "above": 45, + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[0].data["some"] == "is_pos_gt_45 - event - test_event1" + assert calls[1].data["some"] == "is_pos_lt_90 - event - test_event2" + assert calls[2].data["some"] == "is_pos_gt_45_lt_90 - event - test_event3" + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 45} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_pos_lt_90 - event - test_event2" + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 90} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data["some"] == "is_pos_gt_45 - event - test_event1" + + +async def test_if_tilt_position(hass, calls): + """Test for tilt position conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_tilt_position", + "above": 45, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_tilt_position", + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_tilt_position", + "above": 45, + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[0].data["some"] == "is_pos_gt_45 - event - test_event1" + assert calls[1].data["some"] == "is_pos_lt_90 - event - test_event2" + assert calls[2].data["some"] == "is_pos_gt_45_lt_90 - event - test_event3" + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 45} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_pos_lt_90 - event - test_event2" + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 90} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data["some"] == "is_pos_gt_45 - event - test_event1" diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py new file mode 100644 index 00000000000..d7c771e2b28 --- /dev/null +++ b/tests/testing_config/custom_components/test/cover.py @@ -0,0 +1,61 @@ +""" +Provide a mock cover platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.cover import CoverDevice +from tests.common import MockEntity + + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockCover(name=f"Simple cover", is_on=True, unique_id=f"unique_cover"), + MockCover( + name=f"Set position cover", + is_on=True, + unique_id=f"unique_set_pos_cover", + current_cover_position=50, + ), + MockCover( + name=f"Set tilt position cover", + is_on=True, + unique_id=f"unique_set_pos_tilt_cover", + current_cover_tilt_position=50, + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) + + +class MockCover(MockEntity, CoverDevice): + """Mock Cover class.""" + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return False + + @property + def current_cover_position(self): + """Return current position of cover.""" + return self._handle("current_cover_position") + + @property + def current_cover_tilt_position(self): + """Return current position of cover tilt.""" + return self._handle("current_cover_tilt_position") From d6654eaecb651ba895fa56cb7b917456898c87da Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 17:53:33 -0400 Subject: [PATCH 039/306] Implement Alexa.PlaybackStateReporter Interface for alexa (#28215) --- .../components/alexa/capabilities.py | 40 ++++++++++++++++++- homeassistant/components/alexa/entities.py | 2 + tests/components/alexa/test_capabilities.py | 24 +++++++++++ tests/components/alexa/test_smart_home.py | 2 + 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 52dde74ff3a..b5b7fa88ef8 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -12,9 +12,11 @@ from homeassistant.const import ( STATE_LOCKED, STATE_OFF, STATE_ON, + STATE_PAUSED, + STATE_PLAYING, STATE_UNAVAILABLE, - STATE_UNLOCKED, STATE_UNKNOWN, + STATE_UNLOCKED, ) import homeassistant.components.climate.const as climate import homeassistant.components.media_player.const as media_player @@ -1137,3 +1139,39 @@ class AlexaDoorbellEventSource(AlexaCapability): def capability_proactively_reported(self): """Return True for proactively reported capability.""" return True + + +class AlexaPlaybackStateReporter(AlexaCapability): + """Implements Alexa.PlaybackStateReporter. + + https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PlaybackStateReporter" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "playbackState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "playbackState": + raise UnsupportedProperty(name) + + playback_state = self.entity.state + if playback_state == STATE_PLAYING: + return {"state": "PLAYING"} + if playback_state == STATE_PAUSED: + return {"state": "PAUSED"} + + return {"state": "STOPPED"} diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e52e1b4f87e..ef6c2902053 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -46,6 +46,7 @@ from .capabilities import ( AlexaMotionSensor, AlexaPercentageController, AlexaPlaybackController, + AlexaPlaybackStateReporter, AlexaPowerController, AlexaPowerLevelController, AlexaRangeController, @@ -422,6 +423,7 @@ class MediaPlayerCapabilities(AlexaEntity): ) if supported & playback_features: yield AlexaPlaybackController(self.entity) + yield AlexaPlaybackStateReporter(self.entity) if supported & media_player.SUPPORT_SELECT_SOURCE: yield AlexaInputController(self.entity) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 89815c72544..d2f6cddc522 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -17,6 +17,11 @@ from homeassistant.const import ( from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home from homeassistant.components.alexa.errors import UnsupportedProperty +from homeassistant.components.media_player.const import ( + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_STOP, +) from tests.common import async_mock_service from . import ( @@ -642,3 +647,22 @@ async def test_report_alarm_control_panel_state(hass): properties = await reported_properties(hass, "alarm_control_panel.disarmed") properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") + + +async def test_report_playback_state(hass): + """Test PlaybackStateReporter implements playbackState property.""" + hass.states.async_set( + "media_player.test", + "off", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP, + "volume_level": 0.75, + }, + ) + + properties = await reported_properties(hass, "media_player.test") + + properties.assert_equal( + "Alexa.PlaybackStateReporter", "playbackState", {"state": "STOPPED"} + ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 00c762103f3..3994c3d9f5d 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -733,6 +733,7 @@ async def test_media_player(hass): "Alexa.Speaker", "Alexa.StepSpeaker", "Alexa.PlaybackController", + "Alexa.PlaybackStateReporter", "Alexa.EndpointHealth", "Alexa.ChannelController", ) @@ -954,6 +955,7 @@ async def test_media_player_power(hass): "Alexa.Speaker", "Alexa.StepSpeaker", "Alexa.PlaybackController", + "Alexa.PlaybackStateReporter", "Alexa.EndpointHealth", "Alexa.ChannelController", ) From 475b43500ae0ef1c56fcdd45cabf8318362bcd22 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 06:55:42 +0800 Subject: [PATCH 040/306] Add above and below to sensor condition extra_fields (#27364) * Add above and below to sensor condition extra_fields * Change unit_of_measurement to suffix in extra_fields * Check if sensor has unit when getting capabilities * Improve tests --- .../components/device_automation/__init__.py | 6 +- .../components/sensor/device_condition.py | 27 +++++++ .../components/sensor/device_trigger.py | 8 +- .../sensor/test_device_condition.py | 81 +++++++++++++++++++ .../components/sensor/test_device_trigger.py | 35 ++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 0be1c3eb1dd..80e64033295 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -156,7 +156,11 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom # The device automation has no capabilities return {} - capabilities = await getattr(platform, function_name)(hass, automation) + try: + capabilities = await getattr(platform, function_name)(hass, automation) + except InvalidDeviceAutomationConfig: + return {} + capabilities = capabilities.copy() extra_fields = capabilities.get("extra_fields") diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 26479807991..259fb5dbab9 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -2,6 +2,9 @@ from typing import Dict, List import voluptuous as vol +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.core import HomeAssistant from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -141,3 +144,27 @@ def async_condition_from_config( numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] return condition.async_numeric_state_from_config(numeric_state_config) + + +async def async_get_condition_capabilities(hass, config): + """List condition capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + + return { + "extra_fields": vol.Schema( + { + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + } + ) + } diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index b462124165a..73e55340da9 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -3,6 +3,9 @@ import voluptuous as vol import homeassistant.components.automation.numeric_state as numeric_state_automation from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -146,9 +149,12 @@ async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" state = hass.states.get(config[CONF_ENTITY_ID]) unit_of_measurement = ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else "" + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None ) + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + return { "extra_fields": vol.Schema( { diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index e28e487f4ef..f3ff15c3ad9 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -14,6 +14,7 @@ from tests.common import ( mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @@ -73,6 +74,86 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "description": {"suffix": "%"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"suffix": "%"}, + "name": "below", + "optional": True, + "type": "float", + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 1 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + +async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + conditions = [ + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state_not_above_below(hass, calls, caplog): """Test for bad value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index a21839fcebc..b7a921fff18 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -124,6 +124,41 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities +async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + triggers = [ + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_not_on_above_below(hass, calls, caplog): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") From 08cc9fd3754c32cb5dc9f65a4c0ef0accb80700a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Oct 2019 16:04:24 -0700 Subject: [PATCH 041/306] Add cloud account linking support (#28210) * Add cloud account linking support * Update account_link.py --- homeassistant/bootstrap.py | 2 + homeassistant/components/cloud/__init__.py | 8 +- .../components/cloud/account_link.py | 132 +++++++++++++++ homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/manifest.json | 2 +- .../components/somfy/.translations/en.json | 5 + homeassistant/components/somfy/strings.json | 29 ++-- .../helpers/config_entry_oauth2_flow.py | 41 ++++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_account_link.py | 160 ++++++++++++++++++ .../helpers/test_config_entry_oauth2_flow.py | 42 +++++ 13 files changed, 407 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/cloud/account_link.py create mode 100644 tests/components/cloud/test_account_link.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 6118f4f2bd7..312c739cd72 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -33,6 +33,8 @@ STAGE_1_INTEGRATIONS = { "recorder", # To make sure we forward data to other instances "mqtt_eventstream", + # To provide account link implementations + "cloud", } diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index a2c79fdc0a7..2d5a2c8b448 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.loader import bind_hass from homeassistant.util.aiohttp import MockRequest -from . import http_api +from . import account_link, http_api from .client import CloudClient from .const import ( CONF_ACME_DIRECTORY_SERVER, @@ -38,6 +38,7 @@ from .const import ( CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, + CONF_ACCOUNT_LINK_URL, DOMAIN, MODE_DEV, MODE_PROD, @@ -101,6 +102,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(), vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(), + vol.Optional(CONF_ACCOUNT_LINK_URL): vol.Url(), } ) }, @@ -168,7 +170,6 @@ def is_cloudhook_request(request): async def async_setup(hass, config): """Initialize the Home Assistant cloud.""" - # Process configs if DOMAIN in config: kwargs = dict(config[DOMAIN]) @@ -248,4 +249,7 @@ async def async_setup(hass, config): cloud.iot.register_on_connect(_on_connect) await http_api.async_setup(hass) + + account_link.async_setup(hass) + return True diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py new file mode 100644 index 00000000000..6fbfcc8723b --- /dev/null +++ b/homeassistant/components/cloud/account_link.py @@ -0,0 +1,132 @@ +"""Account linking via the cloud.""" +import asyncio +import logging +from typing import Any + +from hass_nabucasa import account_link + +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import event, config_entry_oauth2_flow + +from .const import DOMAIN + +DATA_SERVICES = "cloud_account_link_services" +CACHE_TIMEOUT = 3600 +PATCH_VERSION = int(PATCH_VERSION.split(".")[0]) +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_setup(hass: HomeAssistant): + """Set up cloud account link.""" + config_entry_oauth2_flow.async_add_implementation_provider( + hass, DOMAIN, async_provide_implementation + ) + + +async def async_provide_implementation(hass: HomeAssistant, domain: str): + """Provide an implementation for a domain.""" + services = await _get_services(hass) + + for service in services: + if service["service"] == domain and _is_older(service["min_version"]): + return CloudOAuth2Implementation(hass, domain) + + return + + +@callback +def _is_older(version: str) -> bool: + """Test if a version is older than the current HA version.""" + version_parts = version.split(".") + + if len(version_parts) != 3: + return False + + try: + version_parts = [int(val) for val in version_parts] + except ValueError: + return False + + cur_version_parts = [MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION] + + return version_parts <= cur_version_parts + + +async def _get_services(hass): + """Get the available services.""" + services = hass.data.get(DATA_SERVICES) + + if services is not None: + return services + + services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) + + hass.data[DATA_SERVICES] = services + + @callback + def clear_services(_now): + """Clear services cache.""" + hass.data.pop(DATA_SERVICES, None) + + event.async_call_later(hass, CACHE_TIMEOUT, clear_services) + + return services + + +class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implementation): + """Cloud implementation of the OAuth2 flow.""" + + def __init__(self, hass: HomeAssistant, service: str): + """Initialize cloud OAuth2 implementation.""" + self.hass = hass + self.service = service + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Home Assistant Cloud" + + @property + def domain(self) -> str: + """Domain that is providing the implementation.""" + return DOMAIN + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize.""" + helper = account_link.AuthorizeAccountHelper( + self.hass.data[DOMAIN], self.service + ) + authorize_url = await helper.async_get_authorize_url() + + async def await_tokens(): + """Wait for tokens and pass them on when received.""" + try: + tokens = await helper.async_get_tokens() + + except asyncio.TimeoutError: + _LOGGER.info("Timeout fetching tokens for flow %s", flow_id) + except account_link.AccountLinkException as err: + _LOGGER.info( + "Failed to fetch tokens for flow %s: %s", flow_id, err.code + ) + else: + await self.hass.config_entries.flow.async_configure( + flow_id=flow_id, user_input=tokens + ) + + self.hass.async_create_task(await_tokens()) + + return authorize_url + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve external data to tokens.""" + # We already passed in tokens + return external_data + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh a token.""" + return await account_link.async_fetch_access_token( + self.hass.data[DOMAIN], self.service, token["refresh_token"] + ) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 6495cba23b7..262f84a85e6 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -37,6 +37,7 @@ CONF_REMOTE_API_URL = "remote_api_url" CONF_ACME_DIRECTORY_SERVER = "acme_directory_server" CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url" +CONF_ACCOUNT_LINK_URL = "account_link_url" MODE_DEV = "development" MODE_PROD = "production" diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index c8fa6884563..9e9b77287ae 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.22"], + "requirements": ["hass-nabucasa==0.23"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/somfy/.translations/en.json b/homeassistant/components/somfy/.translations/en.json index d4155915636..3b2f2e6beaf 100644 --- a/homeassistant/components/somfy/.translations/en.json +++ b/homeassistant/components/somfy/.translations/en.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Successfully authenticated with Somfy." }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/strings.json b/homeassistant/components/somfy/strings.json index d4155915636..81308ba18af 100644 --- a/homeassistant/components/somfy/strings.json +++ b/homeassistant/components/somfy/strings.json @@ -1,13 +1,18 @@ { - "config": { - "abort": { - "already_setup": "You can only configure one Somfy account.", - "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Somfy component is not configured. Please follow the documentation." - }, - "create_entry": { - "default": "Successfully authenticated with Somfy." - }, - "title": "Somfy" - } -} \ No newline at end of file + "config": { + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "already_setup": "You can only configure one Somfy account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Somfy." + }, + "title": "Somfy" + } +} diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 7fb954378ee..d3db8febcb2 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -8,7 +8,7 @@ This module exists of the following parts: import asyncio from abc import ABCMeta, ABC, abstractmethod import logging -from typing import Optional, Any, Dict, cast +from typing import Optional, Any, Dict, cast, Awaitable, Callable import time import async_timeout @@ -28,6 +28,7 @@ from .aiohttp_client import async_get_clientsession DATA_JWT_SECRET = "oauth2_jwt_secret" DATA_VIEW_REGISTERED = "oauth2_view_reg" DATA_IMPLEMENTATIONS = "oauth2_impl" +DATA_PROVIDERS = "oauth2_providers" AUTH_CALLBACK_PATH = "/auth/external/callback" @@ -291,11 +292,23 @@ async def async_get_implementations( hass: HomeAssistant, domain: str ) -> Dict[str, AbstractOAuth2Implementation]: """Return OAuth2 implementations for specified domain.""" - return cast( + registered = cast( Dict[str, AbstractOAuth2Implementation], hass.data.setdefault(DATA_IMPLEMENTATIONS, {}).get(domain, {}), ) + if DATA_PROVIDERS not in hass.data: + return registered + + registered = dict(registered) + + for provider_domain, get_impl in hass.data[DATA_PROVIDERS].items(): + implementation = await get_impl(hass, domain) + if implementation is not None: + registered[provider_domain] = implementation + + return registered + async def async_get_config_entry_implementation( hass: HomeAssistant, config_entry: config_entries.ConfigEntry @@ -310,6 +323,23 @@ async def async_get_config_entry_implementation( return implementation +@callback +def async_add_implementation_provider( + hass: HomeAssistant, + provider_domain: str, + async_provide_implementation: Callable[ + [HomeAssistant, str], Awaitable[Optional[AbstractOAuth2Implementation]] + ], +) -> None: + """Add an implementation provider. + + If no implementation found, return None. + """ + hass.data.setdefault(DATA_PROVIDERS, {})[ + provider_domain + ] = async_provide_implementation + + class OAuth2AuthorizeCallbackView(HomeAssistantView): """OAuth2 Authorization Callback View.""" @@ -355,9 +385,14 @@ class OAuth2Session: self.config_entry = config_entry self.implementation = implementation + @property + def token(self) -> dict: + """Return the current token.""" + return cast(dict, self.config_entry.data["token"]) + async def async_ensure_token_valid(self) -> None: """Ensure that the current token is valid.""" - token = self.config_entry.data["token"] + token = self.token if token["expires_at"] > time.time(): return diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fbfa1dbf67b..87878b49615 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 -hass-nabucasa==0.22 +hass-nabucasa==0.23 home-assistant-frontend==20191025.0 importlib-metadata==0.23 jinja2>=2.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index 8e8a72d0181..1039b53f4f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -616,7 +616,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.22 +hass-nabucasa==0.23 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f55cb1aa1b..db5c1a491cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.22 +hass-nabucasa==0.23 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py new file mode 100644 index 00000000000..60116895beb --- /dev/null +++ b/tests/components/cloud/test_account_link.py @@ -0,0 +1,160 @@ +"""Test account link services.""" +import asyncio +import logging +from time import time +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import data_entry_flow, config_entries +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.components.cloud import account_link +from homeassistant.util.dt import utcnow +from tests.common import mock_coro, async_fire_time_changed, mock_platform + + +TEST_DOMAIN = "oauth2_test" + + +@pytest.fixture +def flow_handler(hass): + """Return a registered config flow.""" + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Test flow handler.""" + + DOMAIN = TEST_DOMAIN + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}): + yield TestFlowHandler + + +async def test_setup_provide_implementation(hass): + """Test that we provide implementations.""" + account_link.async_setup(hass) + + with patch( + "homeassistant.components.cloud.account_link._get_services", + side_effect=lambda _: mock_coro( + [ + {"service": "test", "min_version": "0.1.0"}, + {"service": "too_new", "min_version": "100.0.0"}, + ] + ), + ): + assert ( + await config_entry_oauth2_flow.async_get_implementations( + hass, "non_existing" + ) + == {} + ) + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, "too_new") + == {} + ) + implementations = await config_entry_oauth2_flow.async_get_implementations( + hass, "test" + ) + + assert "cloud" in implementations + assert implementations["cloud"].domain == "cloud" + assert implementations["cloud"].service == "test" + assert implementations["cloud"].hass is hass + + +async def test_get_services_cached(hass): + """Test that we cache services.""" + hass.data["cloud"] = None + + services = 1 + + with patch.object(account_link, "CACHE_TIMEOUT", 0), patch( + "hass_nabucasa.account_link.async_fetch_available_services", + side_effect=lambda _: mock_coro(services), + ) as mock_fetch: + assert await account_link._get_services(hass) == 1 + + services = 2 + + assert len(mock_fetch.mock_calls) == 1 + assert await account_link._get_services(hass) == 1 + + services = 3 + hass.data.pop(account_link.DATA_SERVICES) + assert await account_link._get_services(hass) == 3 + + services = 4 + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + # Check cache purged + assert await account_link._get_services(hass) == 4 + + +async def test_implementation(hass, flow_handler): + """Test Cloud OAuth2 implementation.""" + hass.data["cloud"] = None + + impl = account_link.CloudOAuth2Implementation(hass, "test") + assert impl.name == "Home Assistant Cloud" + assert impl.domain == "cloud" + + flow_handler.async_register_implementation(hass, impl) + + flow_finished = asyncio.Future() + + helper = Mock( + async_get_authorize_url=Mock(return_value=mock_coro("http://example.com/auth")), + async_get_tokens=Mock(return_value=flow_finished), + ) + + with patch( + "hass_nabucasa.account_link.AuthorizeAccountHelper", return_value=helper + ): + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == "http://example.com/auth" + + flow_finished.set_result( + { + "refresh_token": "mock-refresh", + "access_token": "mock-access", + "expires_in": 10, + "token_type": "bearer", + } + ) + await hass.async_block_till_done() + + # Flow finished! + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == "cloud" + + expires_at = result["data"]["token"].pop("expires_at") + assert round(expires_at - time()) == 10 + + assert result["data"]["token"] == { + "refresh_token": "mock-refresh", + "access_token": "mock-access", + "token_type": "bearer", + "expires_in": 10, + } + + entry = hass.config_entries.async_entries(TEST_DOMAIN)[0] + + assert ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + is impl + ) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index e47dd834bf7..773dfa09375 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -264,3 +264,45 @@ async def test_oauth_session(hass, flow_handler, local_impl, aioclient_mock): assert config_entry.data["token"]["expires_in"] == 100 assert config_entry.data["token"]["random_other_data"] == "should_stay" assert round(config_entry.data["token"]["expires_at"] - now) == 100 + + +async def test_implementation_provider(hass, local_impl): + """Test providing an implementation provider.""" + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, TEST_DOMAIN) + == {} + ) + + mock_domain_with_impl = "some_domain" + + config_entry_oauth2_flow.async_register_implementation( + hass, mock_domain_with_impl, local_impl + ) + + assert await config_entry_oauth2_flow.async_get_implementations( + hass, mock_domain_with_impl + ) == {TEST_DOMAIN: local_impl} + + provider_source = {} + + async def async_provide_implementation(hass, domain): + """Mock implementation provider.""" + return provider_source.get(domain) + + config_entry_oauth2_flow.async_add_implementation_provider( + hass, "cloud", async_provide_implementation + ) + + assert await config_entry_oauth2_flow.async_get_implementations( + hass, mock_domain_with_impl + ) == {TEST_DOMAIN: local_impl} + + provider_source[ + mock_domain_with_impl + ] = config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, "cloud", CLIENT_ID, CLIENT_SECRET, AUTHORIZE_URL, TOKEN_URL + ) + + assert await config_entry_oauth2_flow.async_get_implementations( + hass, mock_domain_with_impl + ) == {TEST_DOMAIN: local_impl, "cloud": provider_source[mock_domain_with_impl]} From 7096826d1d255778bc4fe6c86895d524e82884fd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 26 Oct 2019 00:32:20 +0000 Subject: [PATCH 042/306] [ci skip] Translation update --- .../cert_expiry/.translations/lb.json | 4 +- .../coolmaster/.translations/ca.json | 7 +++- .../coolmaster/.translations/lb.json | 23 +++++++++++ .../coolmaster/.translations/ru.json | 23 +++++++++++ .../components/cover/.translations/en.json | 4 +- .../device_tracker/.translations/ca.json | 8 ++++ .../device_tracker/.translations/en.json | 8 ++++ .../device_tracker/.translations/ru.json | 8 ++++ .../homematicip_cloud/.translations/ru.json | 2 +- .../huawei_lte/.translations/ca.json | 11 ++++++ .../huawei_lte/.translations/fr.json | 39 +++++++++++++++++++ .../huawei_lte/.translations/lb.json | 39 +++++++++++++++++++ .../huawei_lte/.translations/ru.json | 39 +++++++++++++++++++ .../media_player/.translations/ca.json | 11 ++++++ .../media_player/.translations/en.json | 11 ++++++ .../media_player/.translations/ru.json | 11 ++++++ .../components/sensor/.translations/nl.json | 2 +- .../components/solarlog/.translations/lb.json | 21 ++++++++++ .../transmission/.translations/lb.json | 5 ++- .../transmission/.translations/ru.json | 5 ++- .../components/withings/.translations/ca.json | 1 + .../components/withings/.translations/fr.json | 7 ++++ .../components/withings/.translations/lb.json | 7 ++++ .../components/withings/.translations/nl.json | 5 +++ .../components/withings/.translations/ru.json | 7 ++++ 25 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/lb.json create mode 100644 homeassistant/components/coolmaster/.translations/ru.json create mode 100644 homeassistant/components/device_tracker/.translations/ca.json create mode 100644 homeassistant/components/device_tracker/.translations/en.json create mode 100644 homeassistant/components/device_tracker/.translations/ru.json create mode 100644 homeassistant/components/huawei_lte/.translations/fr.json create mode 100644 homeassistant/components/huawei_lte/.translations/lb.json create mode 100644 homeassistant/components/huawei_lte/.translations/ru.json create mode 100644 homeassistant/components/media_player/.translations/ca.json create mode 100644 homeassistant/components/media_player/.translations/en.json create mode 100644 homeassistant/components/media_player/.translations/ru.json create mode 100644 homeassistant/components/solarlog/.translations/lb.json diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json index 9620526e363..14d12967a38 100644 --- a/homeassistant/components/cert_expiry/.translations/lb.json +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -4,10 +4,12 @@ "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert" }, "error": { + "certificate_error": "Zertifikat konnt net valid\u00e9iert ginn", "certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren", "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.", "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert", - "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn" + "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn", + "wrong_host": "Zertifikat entspr\u00e9cht net den Numm vum Apparat" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/ca.json b/homeassistant/components/coolmaster/.translations/ca.json index c79397b0cc5..e66d887f3e8 100644 --- a/homeassistant/components/coolmaster/.translations/ca.json +++ b/homeassistant/components/coolmaster/.translations/ca.json @@ -1,9 +1,14 @@ { "config": { + "error": { + "connection_error": "No s'ha pogut connectar amb la inst\u00e0ncia de CoolMasterNet. Comprova l'amfitri\u00f3.", + "no_units": "No s'ha pogut trobar cap unitat d'HVAC a l'amfitri\u00f3 de CoolMasterNet." + }, "step": { "user": { "data": { - "host": "Amfitri\u00f3" + "host": "Amfitri\u00f3", + "off": "Es pot apagar" } } }, diff --git a/homeassistant/components/coolmaster/.translations/lb.json b/homeassistant/components/coolmaster/.translations/lb.json new file mode 100644 index 00000000000..ed54abac03e --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Feeler beim verbanne mat der CoolMasterNet Instanz. Iwwerpr\u00e9ift w.e.g. \u00e4ren Apparat.", + "no_units": "Konnt keng HVAC Eenheeten am CoolMasterNet Apparat fannen." + }, + "step": { + "user": { + "data": { + "cool": "\u00cbnnerst\u00ebtzt KillModus", + "dry": "\u00cbnnerst\u00ebtzt Dr\u00e9che Modus", + "fan_only": "\u00cbnnerst\u00ebtzt n\u00ebmmen Ventilatiouns Modus", + "heat": "\u00cbnnerst\u00ebtzt H\u00ebtzt Modus", + "heat_cool": "\u00cbnnerst\u00ebtzt automateschen H\u00ebtzt/Kill Modus", + "host": "Apparat", + "off": "Kann ausgeschalt ginn" + }, + "title": "CoolMasterNet Verbindungs Detailer ariichten" + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/ru.json b/homeassistant/components/coolmaster/.translations/ru.json new file mode 100644 index 00000000000..4c2f74440cd --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430.", + "no_units": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u044f, \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0438\u0438 \u0438 \u043a\u043e\u043d\u0434\u0438\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "cool": "\u0420\u0435\u0436\u0438\u043c \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u044f", + "dry": "\u0420\u0435\u0436\u0438\u043c \u043e\u0441\u0443\u0448\u0435\u043d\u0438\u044f", + "fan_only": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0438\u0438", + "heat": "\u0420\u0435\u0436\u0438\u043c \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430", + "heat_cool": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "host": "\u0425\u043e\u0441\u0442", + "off": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + }, + "title": "CoolMasterNet" + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/en.json b/homeassistant/components/cover/.translations/en.json index f9f47be3104..eaac0d557ef 100644 --- a/homeassistant/components/cover/.translations/en.json +++ b/homeassistant/components/cover/.translations/en.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} is closed", "is_closing": "{entity_name} is closing", "is_open": "{entity_name} is open", - "is_opening": "{entity_name} is opening" + "is_opening": "{entity_name} is opening", + "is_position": "{entity_name} position is", + "is_tilt_position": "{entity_name} tilt position is" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ca.json b/homeassistant/components/device_tracker/.translations/ca.json new file mode 100644 index 00000000000..de5aed41e3c --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u00e9s a casa", + "is_not_home": "{entity_name} no \u00e9s a casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/en.json b/homeassistant/components/device_tracker/.translations/en.json new file mode 100644 index 00000000000..25045e62b15 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} is home", + "is_not_home": "{entity_name} is not home" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ru.json b/homeassistant/components/device_tracker/.translations/ru.json new file mode 100644 index 00000000000..50a48ce942b --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u0434\u043e\u043c\u0430", + "is_not_home": "{entity_name} \u043d\u0435 \u0434\u043e\u043c\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 57ab265d1c2..35f52a7b284 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.", - "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index 5b31875586d..6dc88f21323 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -20,9 +20,20 @@ "url": "URL", "username": "Nom d'usuari" }, + "description": "Introdueix les dades d\u2019acc\u00e9s del dispositiu. El nom d\u2019usuari i contrasenya s\u00f3n opcionals, per\u00f2 habiliten m\u00e9s funcions de la integraci\u00f3. D'altra banda, (mentre la integraci\u00f3 estigui activa) l'\u00fas d'una connexi\u00f3 autoritzada pot causar problemes per accedir a la interf\u00edcie web del dispositiu des de fora de Home Assistant i viceversa.", "title": "Con de Huawei LTE" } }, "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Destinataris de notificacions SMS", + "track_new_devices": "Segueix dispositius nous" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json new file mode 100644 index 00000000000..19f33305356 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "connection_failed": "La connexion a \u00e9chou\u00e9", + "incorrect_password": "Mot de passe incorrect", + "incorrect_username": "Nom d'utilisateur incorrect", + "incorrect_username_or_password": "identifiant ou mot de passe incorrect", + "invalid_url": "URL invalide", + "login_attempts_exceeded": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", + "response_error": "Erreur inconnue de l'appareil", + "unknown_connection_error": "Erreur inconnue lors de la connexion \u00e0 l'appareil" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "url": "URL", + "username": "Nom d'utilisateur" + }, + "description": "Entrez les d\u00e9tails d'acc\u00e8s au p\u00e9riph\u00e9rique. La sp\u00e9cification du nom d'utilisateur et du mot de passe est facultative, mais permet de prendre en charge davantage de fonctionnalit\u00e9s d'int\u00e9gration. En revanche, l\u2019utilisation d\u2019une connexion autoris\u00e9e peut entra\u00eener des probl\u00e8mes d\u2019acc\u00e8s \u00e0 l\u2019interface Web du p\u00e9riph\u00e9rique depuis l\u2019assistant externe lorsque l\u2019int\u00e9gration est active et inversement.", + "title": "Configurer Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Destinataires des notifications SMS", + "track_new_devices": "Suivre de nouveaux appareils" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/lb.json b/homeassistant/components/huawei_lte/.translations/lb.json new file mode 100644 index 00000000000..2b90245e929 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/lb.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebsen Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "connection_failed": "Feeler bei der Verbindung", + "incorrect_password": "Ong\u00ebltegt Passwuert", + "incorrect_username": "Ong\u00ebltege Benotzernumm", + "incorrect_username_or_password": "Ong\u00ebltege Benotzernumm oder Passwuert", + "invalid_url": "Ong\u00eblteg URL", + "login_attempts_exceeded": "Maximal Login Versich iwwerschratt, w.e.g. m\u00e9i sp\u00e9it nach eng K\u00e9ier", + "response_error": "Onbekannte Feeler vum Apparat", + "unknown_connection_error": "Onbekannte Feeler beim verbannen mam Apparat" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "url": "URL", + "username": "Benotzernumm" + }, + "description": "Gitt Detailer fir den Acc\u00e8s op den Apparat an. Benotzernumm a Passwuert si fakultativ, erm\u00e9iglecht awer d'\u00cbnnerst\u00ebtzung fir m\u00e9i Integratiouns Optiounen. Op der anerer S\u00e4it kann d'Benotzung vun enger autoris\u00e9ierter Verbindung Problemer mam Acc\u00e8s zum Web Interface vum Apparat ausserhalb vum Home Assistant verursaachen, w\u00e4rend d'Integratioun aktiv ass.", + "title": "Huawei LTE ariichten" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Empf\u00e4nger vun SMS Notifikatioune", + "track_new_devices": "Nei Apparater verfollegen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json new file mode 100644 index 00000000000..1a0c5cc29ad --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "connection_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "incorrect_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", + "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", + "incorrect_username_or_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "login_attempts_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0432\u0445\u043e\u0434\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u044d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u043a \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437 Home Assistant, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.", + "title": "Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", + "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/ca.json b/homeassistant/components/media_player/.translations/ca.json new file mode 100644 index 00000000000..4889c1781c3 --- /dev/null +++ b/homeassistant/components/media_player/.translations/ca.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est\u00e0 inactiu", + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", + "is_paused": "{entity_name} est\u00e0 en pausa", + "is_playing": "{entity_name} est\u00e0 reproduint" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/en.json b/homeassistant/components/media_player/.translations/en.json new file mode 100644 index 00000000000..472cb98f283 --- /dev/null +++ b/homeassistant/components/media_player/.translations/en.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} is idle", + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", + "is_paused": "{entity_name} is paused", + "is_playing": "{entity_name} is playing" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/ru.json b/homeassistant/components/media_player/.translations/ru.json new file mode 100644 index 00000000000..2b459ccab05 --- /dev/null +++ b/homeassistant/components/media_player/.translations/ru.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f", + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0435", + "is_playing": "{entity_name} \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442 \u043c\u0435\u0434\u0438\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json index 33a7d837d55..796e9c97071 100644 --- a/homeassistant/components/sensor/.translations/nl.json +++ b/homeassistant/components/sensor/.translations/nl.json @@ -9,7 +9,7 @@ "is_signal_strength": "{entity_name} signaalsterkte", "is_temperature": "{entity_name} temperatuur", "is_timestamp": "{entity_name} tijdstip", - "is_value": "{entity_name} waarde" + "is_value": "Huidige {entity_name} waarde" }, "trigger_type": { "battery_level": "{entity_name} batterijniveau", diff --git a/homeassistant/components/solarlog/.translations/lb.json b/homeassistant/components/solarlog/.translations/lb.json new file mode 100644 index 00000000000..8bfaca69d94 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "cannot_connect": "Feeler beim verbannen, iwwerpr\u00e9ift w.e.g d'Adresse vum Apparat" + }, + "step": { + "user": { + "data": { + "host": "De Numm oder IP Adresse vun \u00e4rem Solar-Log Apparat", + "name": "Prefix dee fir \u00e4r Solar-Log Sensoren soll benotz ginn" + }, + "title": "D\u00e9fin\u00e9iert \u00e4r Solar-Log Verbindung" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/lb.json b/homeassistant/components/transmission/.translations/lb.json index 6cc611fcb71..a012bcd8cde 100644 --- a/homeassistant/components/transmission/.translations/lb.json +++ b/homeassistant/components/transmission/.translations/lb.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." }, "error": { "cannot_connect": "Kann sech net mam Server verbannen.", + "name_exists": "Numm g\u00ebtt et schonn", "wrong_credentials": "Falsche Benotzernumm oder Passwuert" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Intervalle vun de Mise \u00e0 jour" }, - "description": "Optioune fir Transmission konfigur\u00e9ieren" + "description": "Optioune fir Transmission konfigur\u00e9ieren", + "title": "Optioune fir Transmission konfigur\u00e9ieren" } } } diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index 23f1ceaaa94..222737b90c9 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission", + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" } } } diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index 89dae4caa1b..21c31ccdaaf 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -11,6 +11,7 @@ "data": { "profile": "Perfil" }, + "description": "Quin perfil has seleccionat al lloc web de Withings? \u00c9s important que els perfils coincideixin sin\u00f3, les dades no s\u2019etiquetaran correctament.", "title": "Perfil d'usuari." }, "user": { diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index ad715d54eb1..ed3a43ae295 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -7,6 +7,13 @@ "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Quel profil avez-vous s\u00e9lectionn\u00e9 sur le site Withings? Il est important que les profils correspondent, sinon les donn\u00e9es seront mal \u00e9tiquet\u00e9es.", + "title": "Profil utilisateur" + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index 5ca969f0391..e6ef316548b 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -7,6 +7,13 @@ "default": "Erfollegr\u00e4ich mam ausgewielte Profile mat Withings authentifiz\u00e9iert." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "W\u00e9ie Profil hutt dir op der Withings Webs\u00e4it ausgewielt? Et ass wichteg dass Profiller passen, soss ginn Donn\u00e9e\u00eb falsch gekennzeechent.", + "title": "Benotzer Profil." + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index 3776621bec2..2ca98656db7 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -7,6 +7,11 @@ "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." }, "step": { + "profile": { + "data": { + "profile": "Profiel" + } + }, "user": { "data": { "profile": "Profiel" diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json index c6c621fbdf3..750e306c89a 100644 --- a/homeassistant/components/withings/.translations/ru.json +++ b/homeassistant/components/withings/.translations/ru.json @@ -7,6 +7,13 @@ "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "step": { + "profile": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" + }, + "description": "\u041a\u0430\u043a\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u0412\u044b \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 Withings? \u0412\u0430\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0444\u0438\u043b\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u043b\u0438, \u0438\u043d\u0430\u0447\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b.", + "title": "Withings" + }, "user": { "data": { "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" From 2baee4ac3ef8ada5e9f2987395f33d9038b2c768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 26 Oct 2019 13:29:36 +0300 Subject: [PATCH 043/306] Add Huawei LTE mobile data switch support (#28188) * Add Huawei LTE mobile data switch support * Remove stale comment * Do HA state updates in base entity --- .../components/huawei_lte/__init__.py | 9 +- homeassistant/components/huawei_lte/const.py | 5 +- homeassistant/components/huawei_lte/switch.py | 109 ++++++++++++++++++ 3 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/huawei_lte/switch.py diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 18f7035a885..e224f45ba90 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -23,6 +23,7 @@ from url_normalize import url_normalize from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT from homeassistant.const import ( CONF_PASSWORD, @@ -48,6 +49,7 @@ from .const import ( KEY_DEVICE_BASIC_INFORMATION, KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, + KEY_DIALUP_MOBILE_DATASWITCH, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, UPDATE_OPTIONS_SIGNAL, @@ -158,6 +160,7 @@ class Router: self.subscriptions.pop(KEY_DEVICE_BASIC_INFORMATION, None) get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) + get_data(KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch) get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) @@ -273,7 +276,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) router.subscriptions.clear() # Forward config entry setup to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN): + for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, domain) ) @@ -316,7 +319,7 @@ async def async_unload_entry( """Unload config entry.""" # Forward config entry unload to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN): + for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): await hass.config_entries.async_forward_entry_unload(config_entry, domain) # Forget about the router and invoke its cleanup @@ -419,7 +422,7 @@ class HuaweiLteBaseEntity(Entity): async def _async_maybe_update(self, url: str) -> None: """Update state if the update signal comes from our router.""" if url == self.router.url: - await self.async_update() + self.async_schedule_update_ha_state(True) async def _async_maybe_update_options(self, config_entry: ConfigEntry) -> None: """Update options if the update signal comes from our router.""" diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 18b8d1a90e1..3d99261e6ed 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -13,6 +13,7 @@ UNIT_SECONDS = "s" KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" +KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_WLAN_HOST_LIST = "wlan_host_list" @@ -24,4 +25,6 @@ SENSOR_KEYS = { KEY_MONITORING_TRAFFIC_STATISTICS, } -ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS +SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} + +ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS | SWITCH_KEYS diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py new file mode 100644 index 00000000000..bff82227b80 --- /dev/null +++ b/homeassistant/components/huawei_lte/switch.py @@ -0,0 +1,109 @@ +"""Support for Huawei LTE switches.""" + +import logging +from typing import Optional + +import attr + +from homeassistant.components.switch import ( + DEVICE_CLASS_SWITCH, + DOMAIN as SWITCH_DOMAIN, + SwitchDevice, +) +from homeassistant.const import CONF_URL +from . import HuaweiLteBaseEntity +from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + switches = [] + + if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): + switches.append(HuaweiLteMobileDataSwitch(router)) + + async_add_entities(switches, True) + + +@attr.s +class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchDevice): + """Huawei LTE switch device base class.""" + + key: str + item: str + _raw_state: Optional[str] = attr.ib(init=False, default=None) + + def _turn(self, state: bool) -> None: + raise NotImplementedError + + def turn_on(self, **kwargs): + """Turn switch on.""" + self._turn(state=True) + + def turn_off(self, **kwargs): + """Turn switch off.""" + self._turn(state=False) + + @property + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_SWITCH + + async def async_added_to_hass(self): + """Subscribe to needed data on add.""" + await super().async_added_to_hass() + self.router.subscriptions[self.key].add(f"{SWITCH_DOMAIN}/{self.item}") + + async def async_will_remove_from_hass(self): + """Unsubscribe from needed data on remove.""" + await super().async_will_remove_from_hass() + self.router.subscriptions[self.key].remove(f"{SWITCH_DOMAIN}/{self.item}") + + async def async_update(self): + """Update state.""" + try: + value = self.router.data[self.key][self.item] + except KeyError: + _LOGGER.debug("%s[%s] not in data", self.key, self.item) + self._available = False + return + self._available = True + self._raw_state = str(value) + + +@attr.s +class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): + """Huawei LTE mobile data switch device.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_DIALUP_MOBILE_DATASWITCH + self.item = "dataswitch" + + @property + def _entity_name(self) -> str: + return "Mobile data" + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + @property + def is_on(self) -> bool: + """Return whether the switch is on.""" + return self._raw_state == "1" + + def _turn(self, state: bool) -> None: + value = 1 if state else 0 + self.router.client.dial_up.set_mobile_dataswitch(dataswitch=value) + self._raw_state = str(value) + self.schedule_update_ha_state() + + @property + def icon(self): + """Return switch icon.""" + return "mdi:signal" if self.is_on else "mdi:signal-off" From bb8f139716b24f21b1caca9368c26afef89ca9fa Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 26 Oct 2019 13:45:42 +0200 Subject: [PATCH 044/306] Upgrade speedtest-cli to 2.1.2 (#28216) --- homeassistant/components/speedtestdotnet/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index b35df8ee7c8..b32026d86ef 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -3,7 +3,7 @@ "name": "Speedtestdotnet", "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", "requirements": [ - "speedtest-cli==2.1.1" + "speedtest-cli==2.1.2" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 1039b53f4f3..a0c4b747f41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1813,7 +1813,7 @@ somecomfort==0.5.2 somfy-mylink-synergy==1.0.6 # homeassistant.components.speedtestdotnet -speedtest-cli==2.1.1 +speedtest-cli==2.1.2 # homeassistant.components.spider spiderpy==1.3.1 From 868f88a4e05a9f22cd58fa317a272f3898e7b0c4 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 27 Oct 2019 00:32:10 +0000 Subject: [PATCH 045/306] [ci skip] Translation update --- .../binary_sensor/.translations/fr.json | 2 +- .../cert_expiry/.translations/pl.json | 4 +- .../coolmaster/.translations/ca.json | 8 +++- .../coolmaster/.translations/pl.json | 18 +++++++++ .../components/cover/.translations/ca.json | 4 +- .../components/cover/.translations/ru.json | 4 +- .../cover/.translations/zh-Hant.json | 4 +- .../components/deconz/.translations/ca.json | 1 + .../device_tracker/.translations/zh-Hant.json | 8 ++++ .../components/glances/.translations/pl.json | 18 ++++++++- .../huawei_lte/.translations/zh-Hant.json | 39 +++++++++++++++++++ .../media_player/.translations/zh-Hant.json | 11 ++++++ .../components/sensor/.translations/fr.json | 4 +- .../components/solarlog/.translations/pl.json | 21 ++++++++++ .../components/somfy/.translations/ca.json | 5 +++ .../components/somfy/.translations/fr.json | 5 +++ .../components/somfy/.translations/ru.json | 5 +++ .../somfy/.translations/zh-Hant.json | 5 +++ .../withings/.translations/zh-Hant.json | 7 ++++ 19 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/pl.json create mode 100644 homeassistant/components/device_tracker/.translations/zh-Hant.json create mode 100644 homeassistant/components/huawei_lte/.translations/zh-Hant.json create mode 100644 homeassistant/components/media_player/.translations/zh-Hant.json create mode 100644 homeassistant/components/solarlog/.translations/pl.json diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 4d9bcefbe66..65abfbcd0bd 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -86,7 +86,7 @@ "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", - "turned_on": "{entity_name} activ\u00e9", + "turned_on": "{entity_name} est activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" } diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json index 162c8bf8a0a..e594ed66a3f 100644 --- a/homeassistant/components/cert_expiry/.translations/pl.json +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -4,10 +4,12 @@ "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana" }, "error": { + "certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu", "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z tym hostem", "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", - "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107" + "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107", + "wrong_host": "Certyfikat nie pasuje do nazwy hosta" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/ca.json b/homeassistant/components/coolmaster/.translations/ca.json index e66d887f3e8..65816e696fe 100644 --- a/homeassistant/components/coolmaster/.translations/ca.json +++ b/homeassistant/components/coolmaster/.translations/ca.json @@ -7,9 +7,15 @@ "step": { "user": { "data": { + "cool": "Suporta mode refredar", + "dry": "Suporta mode assecar", + "fan_only": "Suporta nom\u00e9s mode ventiladoci\u00f3", + "heat": "Suporta mode escalfar", + "heat_cool": "Suporta mode escalfar/refredar autom\u00e0tic", "host": "Amfitri\u00f3", "off": "Es pot apagar" - } + }, + "title": "Configuraci\u00f3 de la connexi\u00f3 amb CoolMasterNet." } }, "title": "CoolMasterNet" diff --git a/homeassistant/components/coolmaster/.translations/pl.json b/homeassistant/components/coolmaster/.translations/pl.json new file mode 100644 index 00000000000..8568eac2e55 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 z CoolMasterNet. Sprawd\u017a adres hosta.", + "no_units": "Nie mo\u017cna znale\u017a\u0107 urz\u0105dze\u0144 HVAC na ho\u015bcie CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Obs\u0142uga trybu ch\u0142odzenia", + "dry": "Obs\u0142uga trybu osuszania", + "fan_only": "Obs\u0142uga trybu \"tylko wentylator\"", + "heat": "Obs\u0142uga tryb grzania" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json index ffa9ca1a927..5602ca98ec2 100644 --- a/homeassistant/components/cover/.translations/ca.json +++ b/homeassistant/components/cover/.translations/ca.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} est\u00e0 tancat/da", "is_closing": "{entity_name} est\u00e0 tancan't-se", "is_open": "{entity_name} est\u00e0 obert/a", - "is_opening": "{entity_name} s'est\u00e0 obrint" + "is_opening": "{entity_name} s'est\u00e0 obrint", + "is_position": "La posici\u00f3 de {entity_name} \u00e9s", + "is_tilt_position": "La posici\u00f3 d'inclinaci\u00f3 de {entity_name} \u00e9s" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 46456bb9464..5e89cc6e82e 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", - "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f" + "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "is_position": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438", + "is_tilt_position": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json index 9723d1a0dd6..63910cd208c 100644 --- a/homeassistant/components/cover/.translations/zh-Hant.json +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} \u5df2\u95dc\u9589", "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589", "is_open": "{entity_name} \u5df2\u958b\u555f", - "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f" + "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f", + "is_position": "{entity_name} \u4f4d\u7f6e\u70ba", + "is_tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 41f257e2a78..2900128eedb 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -65,6 +65,7 @@ "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat", + "remote_button_rotation_stopped": "La rotaci\u00f3 del bot\u00f3 \"{subtype}\" s'ha aturat", "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", diff --git a/homeassistant/components/device_tracker/.translations/zh-Hant.json b/homeassistant/components/device_tracker/.translations/zh-Hant.json new file mode 100644 index 00000000000..4092031434c --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u5728\u5bb6", + "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/pl.json b/homeassistant/components/glances/.translations/pl.json index 21052c7acdc..f53d4d413e0 100644 --- a/homeassistant/components/glances/.translations/pl.json +++ b/homeassistant/components/glances/.translations/pl.json @@ -15,8 +15,22 @@ "password": "Has\u0142o", "port": "Port", "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z systemem Glances", - "username": "Nazwa u\u017cytkownika" - } + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Sprawd\u017a certyfikacj\u0119 systemu", + "version": "Glances wersja API (2 lub 3)" + }, + "title": "Konfiguracja Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "description": "Konfiguracja opcji dla Glances" } } } diff --git a/homeassistant/components/huawei_lte/.translations/zh-Hant.json b/homeassistant/components/huawei_lte/.translations/zh-Hant.json new file mode 100644 index 00000000000..795df4e3d6f --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/zh-Hant.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_failed": "\u9023\u7dda\u5931\u6557", + "incorrect_password": "\u5bc6\u78bc\u932f\u8aa4", + "incorrect_username": "\u4f7f\u7528\u8005\u540d\u7a31\u932f\u8aa4", + "incorrect_username_or_password": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4", + "invalid_url": "\u7db2\u5740\u7121\u6548", + "login_attempts_exceeded": "\u5df2\u9054\u5617\u8a66\u767b\u5165\u6700\u5927\u6b21\u6578\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66", + "response_error": "\u4f86\u81ea\u8a2d\u5099\u672a\u77e5\u932f\u8aa4", + "unknown_connection_error": "\u9023\u7dda\u81f3\u8a2d\u5099\u672a\u77e5\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165\u8a2d\u5099\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u8a2d\u5099 Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", + "title": "\u8a2d\u5b9a Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", + "track_new_devices": "\u8ffd\u8e64\u65b0\u8a2d\u5099" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/zh-Hant.json b/homeassistant/components/media_player/.translations/zh-Hant.json new file mode 100644 index 00000000000..abd2f75950b --- /dev/null +++ b/homeassistant/components/media_player/.translations/zh-Hant.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u9592\u7f6e", + "is_off": "{entity_name} \u95dc\u9589", + "is_on": "{entity_name} \u958b\u555f", + "is_paused": "{entity_name} \u5df2\u66ab\u505c", + "is_playing": "{entity_name} \u6b63\u5728\u64ad\u653e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index 56725a59e21..f0060f151dc 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -9,7 +9,7 @@ "is_signal_strength": "{entity_name} force du signal", "is_temperature": "La temp\u00e9rature de {entity_name}", "is_timestamp": "{entity_name} horodatage", - "is_value": "{entity_name} valeur" + "is_value": "La valeur actuelle de {entity_name} " }, "trigger_type": { "battery_level": "Le niveau de la batterie de {entity_name}", @@ -20,7 +20,7 @@ "signal_strength": "{entity_name} force du signal", "temperature": "La temp\u00e9rature de {entity_name}", "timestamp": "{entity_name} horodatage", - "value": "{entity_name} valeur" + "value": "Changements de valeur de {entity_name} " } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/pl.json b/homeassistant/components/solarlog/.translations/pl.json new file mode 100644 index 00000000000..251d183b361 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP urz\u0105dzenia Solar-Log", + "name": "Prefiks dla sensor\u00f3w Solar-Log" + }, + "title": "Zdefiniuj po\u0142\u0105czenie z Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/ca.json b/homeassistant/components/somfy/.translations/ca.json index 14f707ac046..0ca526fde69 100644 --- a/homeassistant/components/somfy/.translations/ca.json +++ b/homeassistant/components/somfy/.translations/ca.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Somfy." }, + "step": { + "pick_implementation": { + "title": "Tria del m\u00e8tode d'autenticaci\u00f3" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/fr.json b/homeassistant/components/somfy/.translations/fr.json index ba873c4f029..29a3eb77853 100644 --- a/homeassistant/components/somfy/.translations/fr.json +++ b/homeassistant/components/somfy/.translations/fr.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Somfy." }, + "step": { + "pick_implementation": { + "title": "Choisir la m\u00e9thode d'authentification" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/ru.json b/homeassistant/components/somfy/.translations/ru.json index 0c8778dc2e5..0cdb43e4241 100644 --- a/homeassistant/components/somfy/.translations/ru.json +++ b/homeassistant/components/somfy/.translations/ru.json @@ -8,6 +8,11 @@ "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/zh-Hant.json b/homeassistant/components/somfy/.translations/zh-Hant.json index f7f7f4a1d8e..3b11e6ef6e0 100644 --- a/homeassistant/components/somfy/.translations/zh-Hant.json +++ b/homeassistant/components/somfy/.translations/zh-Hant.json @@ -8,6 +8,11 @@ "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u8a2d\u5099\u3002" }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index f01109b2d62..77f3efbd4b9 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -7,6 +7,13 @@ "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" }, "step": { + "profile": { + "data": { + "profile": "\u500b\u4eba\u8a2d\u5b9a" + }, + "description": "\u65bc Withings \u7db2\u7ad9\u6240\u9078\u64c7\u7684\u500b\u4eba\u8a2d\u5b9a\u70ba\u4f55\uff1f\u5047\u5982\u500b\u4eba\u8a2d\u5b9a\u4e0d\u7b26\u5408\u7684\u8a71\uff0c\u8cc7\u6599\u5c07\u6703\u6a19\u793a\u932f\u8aa4\u3002", + "title": "\u500b\u4eba\u8a2d\u5b9a\u3002" + }, "user": { "data": { "profile": "\u500b\u4eba\u8a2d\u5b9a" From 2747f08385ef6560347db54d77a663caa5db423f Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 27 Oct 2019 05:18:23 +0100 Subject: [PATCH 046/306] Add available state to unifiled integration (#28189) * Added Unifi Led * fixed manifest * fixed style issue * removed unused setting * added sugested changes. * fixed order * fixed settings that are required * Fix review issues * fix variable name that was too short * Testing something * Reverted to a previous version for testing * Reverted testing changes. * Add available status and increase version of unifiled package version. * No io in init function. --- homeassistant/components/unifiled/light.py | 7 +++++++ homeassistant/components/unifiled/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index 3dd1a8d5dc9..6b0b1e2edf1 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -57,6 +57,7 @@ class UnifiLedLight(Light): self._name = light["name"] self._unique_id = light["id"] self._state = light["status"]["output"] + self._available = light["isOnline"] self._brightness = self._api.convertfrom100to255(light["status"]["led"]) self._features = SUPPORT_BRIGHTNESS @@ -65,6 +66,11 @@ class UnifiLedLight(Light): """Return the display name of this light.""" return self._name + @property + def available(self): + """Return the available state of this light.""" + return self._available + @property def brightness(self): """Return the brightness name of this light.""" @@ -103,3 +109,4 @@ class UnifiLedLight(Light): self._brightness = self._api.convertfrom100to255( self._api.getlightbrightness(self._unique_id) ) + self._available = self._api.getlightavailable(self._unique_id) diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json index fbf05470c6d..927798bd9ce 100644 --- a/homeassistant/components/unifiled/manifest.json +++ b/homeassistant/components/unifiled/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/unifiled", "dependencies": [], "codeowners": ["@florisvdk"], - "requirements": ["unifiled==0.10"] + "requirements": ["unifiled==0.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index a0c4b747f41..d02d11d5f58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1922,7 +1922,7 @@ twentemilieu==0.1.0 twilio==6.32.0 # homeassistant.components.unifiled -unifiled==0.10 +unifiled==0.11 # homeassistant.components.upcloud upcloud-api==0.4.3 From 7e862e4d92b059963afca01d00f1c4249cd302a7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 Oct 2019 10:04:43 +0100 Subject: [PATCH 047/306] Update praw to 6.4.0 (#27324) * Update praw to 6.4.0 * Update requirements_test_all.txt * Fix docstrings * Update tests --- homeassistant/components/reddit/manifest.json | 2 +- homeassistant/components/reddit/sensor.py | 5 +---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reddit/test_sensor.py | 12 +++++++----- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 55baecc486c..8ab4b686da7 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -3,7 +3,7 @@ "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", "requirements": [ - "praw==6.3.1" + "praw==6.4.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index f9c8140f60d..82f622b968e 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import praw import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -51,8 +52,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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] @@ -117,8 +116,6 @@ class RedditSensor(Entity): def update(self): """Update data from Reddit API.""" - import praw - self._subreddit_data = [] try: diff --git a/requirements_all.txt b/requirements_all.txt index d02d11d5f58..a4621d0e45f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -990,7 +990,7 @@ pocketcasts==0.1 postnl_api==1.0.2 # homeassistant.components.reddit -praw==6.3.1 +praw==6.4.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db5c1a491cb..69eaf2d2922 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -354,7 +354,7 @@ plexwebsocket==0.0.1 pmsensor==0.4 # homeassistant.components.reddit -praw==6.3.1 +praw==6.4.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index 0a0247e2585..a421f6f417c 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -3,6 +3,7 @@ import copy import unittest from unittest.mock import patch +from homeassistant.components.reddit import sensor as reddit_sensor from homeassistant.components.reddit.sensor import ( DOMAIN, ATTR_SUBREDDIT, @@ -98,7 +99,7 @@ MOCK_RESULTS_LENGTH = len(MOCK_RESULTS["results"]) class MockPraw: - """Mock class for tmdbsimple library.""" + """Mock class for Reddit library.""" def __init__( self, @@ -112,7 +113,7 @@ class MockPraw: self._data = MOCK_RESULTS def subreddit(self, subreddit: str): - """Return an instance of a sunbreddit.""" + """Return an instance of a subreddit.""" return MockSubreddit(subreddit, self._data) @@ -160,8 +161,9 @@ class TestRedditSetup(unittest.TestCase): @MockDependency("praw") @patch("praw.Reddit", new=MockPraw) def test_setup_with_valid_config(self, mock_praw): - """Test the platform setup with movie configuration.""" - setup_component(self.hass, "sensor", VALID_CONFIG) + """Test the platform setup with Reddit configuration.""" + with patch.object(reddit_sensor, "praw", mock_praw): + setup_component(self.hass, "sensor", VALID_CONFIG) state = self.hass.states.get("sensor.reddit_worldnews") assert int(state.state) == MOCK_RESULTS_LENGTH @@ -186,6 +188,6 @@ class TestRedditSetup(unittest.TestCase): @MockDependency("praw") @patch("praw.Reddit", new=MockPraw) def test_setup_with_invalid_config(self, mock_praw): - """Test the platform setup with invalid movie configuration.""" + """Test the platform setup with invalid Reddit configuration.""" setup_component(self.hass, "sensor", INVALID_SORT_BY_CONFIG) assert not self.hass.states.get("sensor.reddit_worldnews") From a9db2ead3341cc90da482947c9666e6f6c27e326 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 Oct 2019 12:39:36 +0100 Subject: [PATCH 048/306] Suppress traceback (fixes #28243) (#28262) --- homeassistant/components/iss/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index 002b2e958f7..3b8e222c912 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -120,6 +120,6 @@ class IssData: self.next_rise = iss.next_rise(self.latitude, self.longitude) self.number_of_people_in_space = iss.number_of_people_in_space() self.position = iss.current_location() - except requests.exceptions.HTTPError as error: - _LOGGER.error(error) + except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError): + _LOGGER.error("Unable to retrieve data") return False From 6ac7796fb7599565cf44e90fad1efefa56c43d5a Mon Sep 17 00:00:00 2001 From: ZiroNL Date: Sun, 27 Oct 2019 13:07:44 +0100 Subject: [PATCH 049/306] Add charset to imap component. (#28258) --- homeassistant/components/imap/sensor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index a10fefa1b16..db2f528153b 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -23,6 +23,7 @@ _LOGGER = logging.getLogger(__name__) CONF_SERVER = "server" CONF_FOLDER = "folder" CONF_SEARCH = "search" +CONF_CHARSET = "charset" DEFAULT_PORT = 993 @@ -35,6 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_SERVER): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_CHARSET, default="utf-8"): cv.string, vol.Optional(CONF_FOLDER, default="INBOX"): cv.string, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): cv.string, } @@ -49,6 +51,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= config.get(CONF_PASSWORD), config.get(CONF_SERVER), config.get(CONF_PORT), + config.get(CONF_CHARSET), config.get(CONF_FOLDER), config.get(CONF_SEARCH), ) @@ -62,13 +65,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class ImapSensor(Entity): """Representation of an IMAP sensor.""" - def __init__(self, name, user, password, server, port, folder, search): + def __init__(self, name, user, password, server, port, charset, folder, search): """Initialize the sensor.""" self._name = name or user self._user = user self._password = password self._server = server self._port = port + self._charset = charset self._folder = folder self._email_count = None self._search = search @@ -150,7 +154,9 @@ class ImapSensor(Entity): """Check the number of found emails.""" if self._connection: await self._connection.noop() - result, lines = await self._connection.search(self._search) + result, lines = await self._connection.search( + self._search, charset=self._charset + ) if result == "OK": self._email_count = len(lines[0].split()) From 75f94b914761c7a4ba50e710f117761070242dda Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 28 Oct 2019 01:03:26 +0100 Subject: [PATCH 050/306] Reorg and test attributes for HomematicIP Cloud (#28234) * Reorg and test attribute for HomematicIP Cloud * Add dutyCycle check to security_group * Edit test to improve coverage * Add missing flow test * apply suggestion Co-Authored-By: Martin Hjelmare * fix assert condition --- .../homematicip_cloud/binary_sensor.py | 13 +-- .../components/homematicip_cloud/device.py | 20 ++++- .../components/homematicip_cloud/sensor.py | 21 +++++ .../homematicip_cloud/test_binary_sensor.py | 88 ++++++++++++++++--- .../homematicip_cloud/test_config_flow.py | 9 ++ .../homematicip_cloud/test_sensor.py | 35 ++++++++ .../homematicip_cloud/test_switch.py | 2 +- 7 files changed, 169 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index e308f96c208..b5b663055a1 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -39,7 +39,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -48,7 +47,6 @@ ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode" ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position" ATTR_ACCELERATION_SENSOR_SENSITIVITY = "acceleration_sensor_sensitivity" ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE = "acceleration_sensor_trigger_angle" -ATTR_LOW_BATTERY = "low_battery" ATTR_MOISTURE_DETECTED = "moisture_detected" ATTR_MOTION_DETECTED = "motion_detected" ATTR_POWER_MAINS_FAILURE = "power_mains_failure" @@ -59,12 +57,10 @@ ATTR_WATER_LEVEL_DETECTED = "water_level_detected" ATTR_WINDOW_STATE = "window_state" GROUP_ATTRIBUTES = { - "lowBat": ATTR_LOW_BATTERY, "moistureDetected": ATTR_MOISTURE_DETECTED, "motionDetected": ATTR_MOTION_DETECTED, "powerMainsFailure": ATTR_POWER_MAINS_FAILURE, "presenceDetected": ATTR_PRESENCE_DETECTED, - "unreach": ATTR_GROUP_MEMBER_UNREACHABLE, "waterlevelDetected": ATTR_WATER_LEVEL_DETECTED, } @@ -408,17 +404,22 @@ class HomematicipSecuritySensorGroup( def is_on(self) -> bool: """Return true if safety issue detected.""" parent_is_on = super().is_on + if parent_is_on: + return True + if ( - parent_is_on - or self._device.powerMainsFailure + self._device.powerMainsFailure or self._device.moistureDetected or self._device.waterlevelDetected or self._device.lowBat + or self._device.dutyCycle ): 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/device.py b/homeassistant/components/homematicip_cloud/device.py index b05c0e06928..6c81775b688 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -15,6 +15,9 @@ from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" +ATTR_LOW_BATTERY = "low_battery" +ATTR_CONFIG_PENDING = "config_pending" +ATTR_DUTY_CYCLE_REACHED = "duty_cycle_reached" ATTR_ID = "id" ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device @@ -26,27 +29,40 @@ ATTR_GROUP_MEMBER_UNREACHABLE = "group_member_unreachable" ATTR_DEVICE_OVERHEATED = "device_overheated" ATTR_DEVICE_OVERLOADED = "device_overloaded" ATTR_DEVICE_UNTERVOLTAGE = "device_undervoltage" +ATTR_EVENT_DELAY = "event_delay" DEVICE_ATTRIBUTE_ICONS = { "lowBat": "mdi:battery-outline", - "sabotage": "mdi:alert", + "sabotage": "mdi:shield-alert", + "dutyCycle": "mdi:alert", "deviceOverheated": "mdi:alert", "deviceOverloaded": "mdi:alert", "deviceUndervoltage": "mdi:alert", + "configPending": "mdi:alert-circle", } DEVICE_ATTRIBUTES = { "modelType": ATTR_MODEL_TYPE, "sabotage": ATTR_SABOTAGE, + "dutyCycle": ATTR_DUTY_CYCLE_REACHED, "rssiDeviceValue": ATTR_RSSI_DEVICE, "rssiPeerValue": ATTR_RSSI_PEER, "deviceOverheated": ATTR_DEVICE_OVERHEATED, "deviceOverloaded": ATTR_DEVICE_OVERLOADED, "deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE, + "configPending": ATTR_CONFIG_PENDING, + "eventDelay": ATTR_EVENT_DELAY, "id": ATTR_ID, } -GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE} +GROUP_ATTRIBUTES = { + "modelType": ATTR_MODEL_TYPE, + "lowBat": ATTR_LOW_BATTERY, + "sabotage": ATTR_SABOTAGE, + "dutyCycle": ATTR_DUTY_CYCLE_REACHED, + "configPending": ATTR_CONFIG_PENDING, + "unreach": ATTR_GROUP_MEMBER_UNREACHABLE, +} class HomematicipGenericDevice(Entity): diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 9caa72ba15f..acbf72f6ae9 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -39,12 +39,21 @@ from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) +ATTR_CURRENT_ILLUMINATION = "current_illumination" +ATTR_LOWEST_ILLUMINATION = "lowest_illumination" +ATTR_HIGHEST_ILLUMINATION = "highest_illumination" ATTR_LEFT_COUNTER = "left_counter" ATTR_RIGHT_COUNTER = "right_counter" ATTR_TEMPERATURE_OFFSET = "temperature_offset" ATTR_WIND_DIRECTION = "wind_direction" ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree" +ILLUMINATION_DEVICE_ATTRIBUTES = { + "currentIllumination": ATTR_CURRENT_ILLUMINATION, + "lowestIllumination": ATTR_LOWEST_ILLUMINATION, + "highestIllumination": ATTR_HIGHEST_ILLUMINATION, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud sensors devices.""" @@ -273,6 +282,18 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return "lx" + @property + def device_state_attributes(self): + """Return the state attributes of the wind speed sensor.""" + state_attr = super().device_state_attributes + + for attr, attr_key in ILLUMINATION_DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + return state_attr + class HomematicipPowerSensor(HomematicipGenericDevice): """Representation of a HomematicIP power measuring device.""" diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 0760518171e..38358f9ddff 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -8,8 +8,19 @@ from homeassistant.components.homematicip_cloud.binary_sensor import ( ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, ATTR_ACCELERATION_SENSOR_SENSITIVITY, ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE, - ATTR_LOW_BATTERY, + ATTR_MOISTURE_DETECTED, ATTR_MOTION_DETECTED, + ATTR_POWER_MAINS_FAILURE, + ATTR_PRESENCE_DETECTED, + ATTR_WATER_LEVEL_DETECTED, + ATTR_WINDOW_STATE, +) +from homeassistant.components.homematicip_cloud.device import ( + ATTR_EVENT_DELAY, + ATTR_GROUP_MEMBER_UNREACHABLE, + ATTR_LOW_BATTERY, + ATTR_RSSI_DEVICE, + ATTR_SABOTAGE, ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -105,6 +116,13 @@ async def test_hmip_shutter_contact(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + # test common attributes + assert ha_state.attributes[ATTR_RSSI_DEVICE] == -54 + assert not ha_state.attributes.get(ATTR_SABOTAGE) + await async_manipulate_test_data(hass, hmip_device, "sabotage", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_SABOTAGE] + async def test_hmip_motion_detector(hass, default_mock_hap): """Test HomematicipMotionDetector.""" @@ -137,6 +155,11 @@ async def test_hmip_presence_detector(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON + assert not ha_state.attributes.get(ATTR_EVENT_DELAY) + await async_manipulate_test_data(hass, hmip_device, "eventDelay", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_EVENT_DELAY] + async def test_hmip_smoke_detector(hass, default_mock_hap): """Test HomematicipSmokeDetector.""" @@ -267,10 +290,25 @@ async def test_hmip_security_zone_sensor_group(hass, default_mock_hap): ) assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_MOTION_DETECTED) + assert not ha_state.attributes.get(ATTR_PRESENCE_DETECTED) + assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) + assert not ha_state.attributes.get(ATTR_SABOTAGE) + assert not ha_state.attributes.get(ATTR_WINDOW_STATE) + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + await async_manipulate_test_data(hass, hmip_device, "presenceDetected", True) + await async_manipulate_test_data(hass, hmip_device, "unreach", True) + await async_manipulate_test_data(hass, hmip_device, "sabotage", True) + await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN) ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_MOTION_DETECTED] is True + assert ha_state.attributes[ATTR_MOTION_DETECTED] + assert ha_state.attributes[ATTR_PRESENCE_DETECTED] + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] + assert ha_state.attributes[ATTR_SABOTAGE] + assert ha_state.attributes[ATTR_WINDOW_STATE] == WindowState.OPEN async def test_hmip_security_sensor_group(hass, default_mock_hap): @@ -283,14 +321,6 @@ async def test_hmip_security_sensor_group(hass, default_mock_hap): hass, default_mock_hap, entity_id, entity_name, device_model ) - assert ha_state.state == STATE_OFF - assert not ha_state.attributes.get("low_bat") - await async_manipulate_test_data(hass, hmip_device, "lowBat", True) - ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_LOW_BATTERY] is True - - await async_manipulate_test_data(hass, hmip_device, "lowBat", False) await async_manipulate_test_data( hass, hmip_device, @@ -299,7 +329,45 @@ async def test_hmip_security_sensor_group(hass, default_mock_hap): ) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON + assert ( ha_state.attributes["smoke_detector_alarm"] == SmokeDetectorAlarmType.PRIMARY_ALARM ) + await async_manipulate_test_data( + hass, hmip_device, "smokeDetectorAlarmType", SmokeDetectorAlarmType.IDLE_OFF + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + assert not ha_state.attributes.get(ATTR_LOW_BATTERY) + assert not ha_state.attributes.get(ATTR_MOTION_DETECTED) + assert not ha_state.attributes.get(ATTR_PRESENCE_DETECTED) + assert not ha_state.attributes.get(ATTR_POWER_MAINS_FAILURE) + assert not ha_state.attributes.get(ATTR_MOISTURE_DETECTED) + assert not ha_state.attributes.get(ATTR_WATER_LEVEL_DETECTED) + assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) + assert not ha_state.attributes.get(ATTR_SABOTAGE) + assert not ha_state.attributes.get(ATTR_WINDOW_STATE) + + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + await async_manipulate_test_data(hass, hmip_device, "presenceDetected", True) + await async_manipulate_test_data(hass, hmip_device, "powerMainsFailure", True) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True) + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True) + await async_manipulate_test_data(hass, hmip_device, "unreach", True) + await async_manipulate_test_data(hass, hmip_device, "sabotage", True) + await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_LOW_BATTERY] + assert ha_state.attributes[ATTR_MOTION_DETECTED] + assert ha_state.attributes[ATTR_PRESENCE_DETECTED] + assert ha_state.attributes[ATTR_POWER_MAINS_FAILURE] + assert ha_state.attributes[ATTR_MOISTURE_DETECTED] + assert ha_state.attributes[ATTR_WATER_LEVEL_DETECTED] + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] + assert ha_state.attributes[ATTR_SABOTAGE] + assert ha_state.attributes[ATTR_WINDOW_STATE] == WindowState.OPEN diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index 54cb309755d..afaf71c67b5 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -98,6 +98,15 @@ async def test_init_flow_show_form(hass): assert result["type"] == "form" +async def test_init_flow_user_show_form(hass): + """Test config flow shows up with a form.""" + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + assert result["type"] == "form" + + async def test_init_already_configured(hass): """Test accesspoint is already configured.""" MockConfigEntry( diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 8412cd19f4d..f0a81c69074 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -2,8 +2,20 @@ from homematicip.base.enums import ValveState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.device import ( + ATTR_CONFIG_PENDING, + ATTR_DEVICE_OVERHEATED, + ATTR_DEVICE_OVERLOADED, + ATTR_DEVICE_UNTERVOLTAGE, + ATTR_DUTY_CYCLE_REACHED, + ATTR_RSSI_DEVICE, + ATTR_RSSI_PEER, +) from homeassistant.components.homematicip_cloud.sensor import ( + ATTR_CURRENT_ILLUMINATION, + ATTR_HIGHEST_ILLUMINATION, ATTR_LEFT_COUNTER, + ATTR_LOWEST_ILLUMINATION, ATTR_RIGHT_COUNTER, ATTR_TEMPERATURE_OFFSET, ATTR_WIND_DIRECTION, @@ -92,6 +104,9 @@ async def test_hmip_humidity_sensor(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "humidity", 45) ha_state = hass.states.get(entity_id) assert ha_state.state == "45" + # test common attributes + assert ha_state.attributes[ATTR_RSSI_DEVICE] == -76 + assert ha_state.attributes[ATTR_RSSI_PEER] == -77 async def test_hmip_temperature_sensor1(hass, default_mock_hap): @@ -153,6 +168,23 @@ async def test_hmip_power_sensor(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" + # test common attributes + assert not ha_state.attributes.get(ATTR_DEVICE_OVERHEATED) + assert not ha_state.attributes.get(ATTR_DEVICE_OVERLOADED) + assert not ha_state.attributes.get(ATTR_DEVICE_UNTERVOLTAGE) + assert not ha_state.attributes.get(ATTR_DUTY_CYCLE_REACHED) + assert not ha_state.attributes.get(ATTR_CONFIG_PENDING) + await async_manipulate_test_data(hass, hmip_device, "deviceOverheated", True) + await async_manipulate_test_data(hass, hmip_device, "deviceOverloaded", True) + await async_manipulate_test_data(hass, hmip_device, "deviceUndervoltage", True) + await async_manipulate_test_data(hass, hmip_device, "dutyCycle", True) + await async_manipulate_test_data(hass, hmip_device, "configPending", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_DEVICE_OVERHEATED] + assert ha_state.attributes[ATTR_DEVICE_OVERLOADED] + assert ha_state.attributes[ATTR_DEVICE_UNTERVOLTAGE] + assert ha_state.attributes[ATTR_DUTY_CYCLE_REACHED] + assert ha_state.attributes[ATTR_CONFIG_PENDING] async def test_hmip_illuminance_sensor1(hass, default_mock_hap): @@ -187,6 +219,9 @@ async def test_hmip_illuminance_sensor2(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "averageIllumination", 231) ha_state = hass.states.get(entity_id) assert ha_state.state == "231" + assert ha_state.attributes[ATTR_CURRENT_ILLUMINATION] == 785.2 + assert ha_state.attributes[ATTR_HIGHEST_ILLUMINATION] == 837.1 + assert ha_state.attributes[ATTR_LOWEST_ILLUMINATION] == 785.2 async def test_hmip_windspeed_sensor(hass, default_mock_hap): diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index 9e33d1d9587..b8ca7b4b67e 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -136,7 +136,7 @@ async def test_hmip_group_switch(hass, default_mock_hap): assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) await async_manipulate_test_data(hass, hmip_device, "unreach", True) ha_state = hass.states.get(entity_id) - assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] is True + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] async def test_hmip_multi_switch(hass, default_mock_hap): From 72dee7dd211a313f9422a1b0fdd040358834f6cc Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 28 Oct 2019 00:32:16 +0000 Subject: [PATCH 051/306] [ci skip] Translation update --- .../alarm_control_panel/.translations/ru.json | 4 ++ .../cert_expiry/.translations/ko.json | 4 +- .../coolmaster/.translations/ko.json | 23 +++++++++++ .../components/cover/.translations/fr.json | 4 +- .../components/cover/.translations/ko.json | 4 +- .../device_tracker/.translations/fr.json | 8 ++++ .../device_tracker/.translations/ko.json | 8 ++++ .../huawei_lte/.translations/ko.json | 39 +++++++++++++++++++ .../media_player/.translations/fr.json | 11 ++++++ .../media_player/.translations/ko.json | 11 ++++++ .../components/sensor/.translations/ko.json | 36 ++++++++--------- .../components/solarlog/.translations/ko.json | 21 ++++++++++ .../components/somfy/.translations/ko.json | 5 +++ .../transmission/.translations/ko.json | 5 ++- .../components/withings/.translations/ko.json | 7 ++++ .../components/zha/.translations/ru.json | 7 ++++ 16 files changed, 175 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/ko.json create mode 100644 homeassistant/components/device_tracker/.translations/fr.json create mode 100644 homeassistant/components/device_tracker/.translations/ko.json create mode 100644 homeassistant/components/huawei_lte/.translations/ko.json create mode 100644 homeassistant/components/media_player/.translations/fr.json create mode 100644 homeassistant/components/media_player/.translations/ko.json create mode 100644 homeassistant/components/solarlog/.translations/ko.json diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json index acea0ae7551..e573ce70918 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ru.json +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -1,6 +1,10 @@ { "device_automation": { "action_type": { + "arm_away": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "arm_home": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" } } diff --git a/homeassistant/components/cert_expiry/.translations/ko.json b/homeassistant/components/cert_expiry/.translations/ko.json index a807d32a6fb..25c518f8629 100644 --- a/homeassistant/components/cert_expiry/.translations/ko.json +++ b/homeassistant/components/cert_expiry/.translations/ko.json @@ -4,10 +4,12 @@ "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "certificate_error": "\uc778\uc99d\uc11c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "wrong_host": "\uc778\uc99d\uc11c\uac00 \ud638\uc2a4\ud2b8 \uc774\ub984\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/ko.json b/homeassistant/components/coolmaster/.translations/ko.json new file mode 100644 index 00000000000..ff6ddf0acfe --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "CoolMasterNet \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "no_units": "CoolMasterNet \ud638\uc2a4\ud2b8\uc5d0\uc11c HVAC \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "cool": "\ub0c9\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", + "dry": "\uc81c\uc2b5 \ubaa8\ub4dc \uc9c0\uc6d0", + "fan_only": "\uc1a1\ud48d \ubaa8\ub4dc \uc9c0\uc6d0", + "heat": "\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", + "heat_cool": "\uc790\ub3d9 \ub0c9/\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", + "host": "\ud638\uc2a4\ud2b8", + "off": "\uc804\uc6d0\uc744 \ub04c \uc218 \uc788\uc2b4" + }, + "title": "CoolMasterNet \uc5f0\uacb0 \uc0c1\uc138\uc815\ubcf4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json index 95978ed0fa5..1f2debe9c04 100644 --- a/homeassistant/components/cover/.translations/fr.json +++ b/homeassistant/components/cover/.translations/fr.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} est ferm\u00e9", "is_closing": "{entity_name} se ferme", "is_open": "{entity_name} est ouvert", - "is_opening": "{entity_name} est en train de s'ouvrir" + "is_opening": "{entity_name} est en train de s'ouvrir", + "is_position": "La position de {entity_name} est", + "is_tilt_position": "La position d'inclinaison de {entity_name} est" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 02f900a8fe5..48f7ba17532 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", - "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4" + "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", + "is_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\ub294", + "is_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\ub294" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/fr.json b/homeassistant/components/device_tracker/.translations/fr.json new file mode 100644 index 00000000000..bf9033170c1 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/fr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} est \u00e0 la maison", + "is_not_home": "{entity_name} n'est pas \u00e0 la maison" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ko.json b/homeassistant/components/device_tracker/.translations/ko.json new file mode 100644 index 00000000000..34389297f28 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/ko.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \ub2d8\uc774 \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", + "is_not_home": "{entity_name} \ub2d8\uc774 \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/ko.json b/homeassistant/components/huawei_lte/.translations/ko.json new file mode 100644 index 00000000000..b21e0aa0a23 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/ko.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "connection_failed": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "incorrect_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "incorrect_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "incorrect_username_or_password": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "invalid_url": "URL \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "login_attempts_exceeded": "\ucd5c\ub300 \ub85c\uadf8\uc778 \uc2dc\ub3c4 \ud69f\uc218\ub97c \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "response_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "unknown_connection_error": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \uc911 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "url": "URL \uc8fc\uc18c", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d \ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654 \ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4\ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "Huawei LTE \uc124\uc815" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790", + "track_new_devices": "\uc0c8\ub85c\uc6b4 \uae30\uae30 \ucd94\uc801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/fr.json b/homeassistant/components/media_player/.translations/fr.json new file mode 100644 index 00000000000..6be3e609590 --- /dev/null +++ b/homeassistant/components/media_player/.translations/fr.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est inactif", + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9", + "is_paused": "{entity_name} est en pause", + "is_playing": "{entity_name} joue" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/ko.json b/homeassistant/components/media_player/.translations/ko.json new file mode 100644 index 00000000000..7542154448f --- /dev/null +++ b/homeassistant/components/media_player/.translations/ko.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734\uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", + "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd\uc911\uc785\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json index d24a4058343..0e74f3f4f89 100644 --- a/homeassistant/components/sensor/.translations/ko.json +++ b/homeassistant/components/sensor/.translations/ko.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", - "is_humidity": "{entity_name} \uc2b5\ub3c4", - "is_illuminance": "{entity_name} \uc870\ub3c4", - "is_power": "{entity_name} \uc18c\ube44 \uc804\ub825", - "is_pressure": "{entity_name} \uc555\ub825", - "is_signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", - "is_temperature": "{entity_name} \uc628\ub3c4", - "is_timestamp": "{entity_name} \uc2dc\uac01", - "is_value": "{entity_name} \uac12" + "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", + "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4", + "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4", + "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825", + "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825", + "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4", + "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4", + "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01", + "is_value": "\ud604\uc7ac {entity_name} \uac12" }, "trigger_type": { - "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", - "humidity": "{entity_name} \uc2b5\ub3c4", - "illuminance": "{entity_name} \uc870\ub3c4", - "power": "{entity_name} \uc18c\ube44 \uc804\ub825", - "pressure": "{entity_name} \uc555\ub825", - "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", - "temperature": "{entity_name} \uc628\ub3c4", - "timestamp": "{entity_name} \uc2dc\uac01", - "value": "{entity_name} \uac12" + "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubcc0\ud654", + "humidity": "{entity_name} \uc2b5\ub3c4 \ubcc0\ud654", + "illuminance": "{entity_name} \uc870\ub3c4 \ubcc0\ud654", + "power": "{entity_name} \uc18c\ube44 \uc804\ub825 \ubcc0\ud654", + "pressure": "{entity_name} \uc555\ub825 \ubcc0\ud654", + "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4 \ubcc0\ud654", + "temperature": "{entity_name} \uc628\ub3c4 \ubcc0\ud654", + "timestamp": "{entity_name} \uc2dc\uac01 \ubcc0\ud654", + "value": "{entity_name} \uac12 \ubcc0\ud654" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/ko.json b/homeassistant/components/solarlog/.translations/ko.json new file mode 100644 index 00000000000..ea337d1e675 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "data": { + "host": "Solar-Log \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c", + "name": "Solar-Log \uc13c\uc11c\uc5d0 \uc0ac\uc6a9\ub420 \uc811\ub450\uc0ac" + }, + "title": "Solar-Log \uc5f0\uacb0 \uc815\uc758" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/ko.json b/homeassistant/components/somfy/.translations/ko.json index 72b234cd98b..c7fa0e4d293 100644 --- a/homeassistant/components/somfy/.translations/ko.json +++ b/homeassistant/components/somfy/.translations/ko.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Somfy \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ko.json b/homeassistant/components/transmission/.translations/ko.json index a9b1b369f90..507d4e84789 100644 --- a/homeassistant/components/transmission/.translations/ko.json +++ b/homeassistant/components/transmission/.translations/ko.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "error": { "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", "wrong_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" }, - "description": "Transmission \uc635\uc158 \uc124\uc815" + "description": "Transmission \uc635\uc158 \uc124\uc815", + "title": "Transmission \uc635\uc158 \uc124\uc815" } } } diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json index 617964e0596..4191e03d440 100644 --- a/homeassistant/components/withings/.translations/ko.json +++ b/homeassistant/components/withings/.translations/ko.json @@ -7,6 +7,13 @@ "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { + "profile": { + "data": { + "profile": "\ud504\ub85c\ud544" + }, + "description": "Withings \uc6f9 \uc0ac\uc774\ud2b8\uc5d0\uc11c \uc5b4\ub5a4 \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud558\uc168\ub098\uc694? \ud504\ub85c\ud544\uc774 \uc77c\uce58\ud574\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74, \ub370\uc774\ud130\uc5d0 \ub808\uc774\ube14\uc774 \uc798\ubabb \uc9c0\uc815\ub429\ub2c8\ub2e4.", + "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544." + }, "user": { "data": { "profile": "\ud504\ub85c\ud544" diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 1779ed613fc..983a69cdc2c 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -33,6 +33,13 @@ "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "face_1": "\u043d\u0430 \u043f\u0435\u0440\u0432\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_2": "\u043d\u0430 \u0432\u0442\u043e\u0440\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_3": "\u043d\u0430 \u0442\u0440\u0435\u0442\u0435\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_4": "\u043d\u0430 \u0447\u0435\u0442\u0432\u0451\u0440\u0442\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_5": "\u043d\u0430 \u043f\u044f\u0442\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_6": "\u043d\u0430 \u0448\u0435\u0441\u0442\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_any": "\u043d\u0430 \u043b\u044e\u0431\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", From edcf476408f495b62ba2b3d7963f129546da9d2d Mon Sep 17 00:00:00 2001 From: fwestenberg <47930023+fwestenberg@users.noreply.github.com> Date: Mon, 28 Oct 2019 07:43:01 +0100 Subject: [PATCH 052/306] Add support for Xiaomi Air Quality Monitor (cgllc.airmonitor.b1) (#27735) --- .../components/xiaomi_miio/air_quality.py | 193 ++++++++++++++++++ .../components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/air_quality.py diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py new file mode 100644 index 00000000000..e96ed074002 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -0,0 +1,193 @@ +"""Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" +from miio import AirQualityMonitor, DeviceException +import voluptuous as vol + +from homeassistant.components.air_quality import ( + AirQualityEntity, + PLATFORM_SCHEMA, + _LOGGER, + ATTR_PM_2_5, +) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, ATTR_TEMPERATURE +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv + +DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor" +DATA_KEY = "air_quality.xiaomi_miio" + +ATTR_CO2E = "carbon_dioxide_equivalent" +ATTR_HUMIDITY = "relative_humidity" +ATTR_TVOC = "total_volatile_organic_compounds" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_SW_VERSION = "sw_version" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + +PROP_TO_ATTR = { + "carbon_dioxide_equivalent": ATTR_CO2E, + "relative_humidity": ATTR_HUMIDITY, + "particulate_matter_2_5": ATTR_PM_2_5, + "temperature": ATTR_TEMPERATURE, + "total_volatile_organic_compounds": ATTR_TVOC, + "manufacturer": ATTR_MANUFACTURER, + "model": ATTR_MODEL, + "sw_version": ATTR_SW_VERSION, +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the sensor from config.""" + + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + host = config.get(CONF_HOST) + token = config.get(CONF_TOKEN) + name = config.get(CONF_NAME) + + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + + try: + device = AirMonitorB1(name, AirQualityMonitor(host, token, model=None)) + + except DeviceException: + raise PlatformNotReady + + hass.data[DATA_KEY][host] = device + async_add_entities([device], update_before_add=True) + + +class AirMonitorB1(AirQualityEntity): + """Air Quality class for Xiaomi cgllc.airmonitor.b1 device.""" + + def __init__(self, name, device): + """Initialize the entity.""" + self._name = name + self._device = device + self._icon = "mdi:cloud" + self._manufacturer = "Xiaomi" + self._unit_of_measurement = "μg/m3" + self._model = None + self._mac_address = None + self._sw_version = None + self._carbon_dioxide_equivalent = None + self._relative_humidity = None + self._particulate_matter_2_5 = None + self._temperature = None + self._total_volatile_organic_compounds = None + + async def async_update(self): + """Fetch state from the miio device.""" + + try: + if self._model is None: + info = await self.hass.async_add_executor_job(self._device.info) + self._model = info.model + self._mac_address = info.mac_address + self._sw_version = info.firmware_version + + state = await self.hass.async_add_executor_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._carbon_dioxide_equivalent = state.co2e + self._relative_humidity = round(state.humidity, 1) + self._particulate_matter_2_5 = round(state.pm25, 1) + self._temperature = round(state.temperature, 1) + self._total_volatile_organic_compounds = round(state.tvoc, 3) + + except DeviceException as ex: + _LOGGER.error("Got exception while fetching the state: %s", ex) + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def device(self): + """Return the name of this entity, if any.""" + return self._device + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def manufacturer(self): + """Return the manufacturer version.""" + return self._manufacturer + + @property + def model(self): + """Return the device model.""" + return self._model + + @property + def sw_version(self): + """Return the software version.""" + return self._sw_version + + @property + def mac_address(self): + """Return the mac address.""" + return self._mac_address + + @property + def unique_id(self): + """Return the unique ID.""" + return f"{self._model}-{self._mac_address}" + + @property + def carbon_dioxide_equivalent(self): + """Return the CO2e (carbon dioxide equivalent) level.""" + return self._carbon_dioxide_equivalent + + @property + def relative_humidity(self): + """Return the humidity percentage.""" + return self._relative_humidity + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._particulate_matter_2_5 + + @property + def temperature(self): + """Return the temperature in °C.""" + return self._temperature + + @property + def total_volatile_organic_compounds(self): + """Return the total volatile organic compounds.""" + return self._total_volatile_organic_compounds + + @property + def state_attributes(self): + """Return the state attributes.""" + data = {} + + for prop, attr in PROP_TO_ATTR.items(): + value = getattr(self, prop) + if value is not None: + data[attr] = value + + return data + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the current state.""" + return self._particulate_matter_2_5 diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index b675e6e6746..849e4573bbf 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", - "python-miio==0.4.6" + "python-miio==0.4.7" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a4621d0e45f..b589ef6d918 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1537,7 +1537,7 @@ python-juicenet==0.0.5 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.4.6 +python-miio==0.4.7 # homeassistant.components.mpd python-mpd2==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69eaf2d2922..861e1098664 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -522,7 +522,7 @@ python-forecastio==1.4.0 python-izone==1.1.1 # homeassistant.components.xiaomi_miio -python-miio==0.4.6 +python-miio==0.4.7 # homeassistant.components.nest python-nest==4.1.0 From 54342d2a4e77eeedd956de3f1613283ede9ce38e Mon Sep 17 00:00:00 2001 From: "J.P. Hutchins" <34154542+JPHutchins@users.noreply.github.com> Date: Mon, 28 Oct 2019 02:20:59 -0700 Subject: [PATCH 053/306] Add transmission info about torrents that is accessible with templating (#27111) * Add information about current downloads. * Cleanup: add "Torrent Info" state attribute * Add username to codeowners * Rename state_attributes - device_state_attributes. * Fix snakecase keys, use f-strings, remove redundant method. * Access started_torrent_dict directly * Add return None condition * Remove redundancy. * Add missing comma in codeowners list. * Add missing @ to username. * Update CODEOWNERS with script.hassfest. * Remove transmission_downloading, give started_torrents the info. * Confirm changes. * Actually approve changes. * Resolve conflicts. * Remove leftovers from old torrent_info sensor. * Remove get_started_torrent_info method. Old method to display boolean for the removed torrent_info sensor. --- CODEOWNERS | 2 +- .../components/transmission/__init__.py | 27 +++++++++++++++++++ .../components/transmission/const.py | 1 + .../components/transmission/manifest.json | 5 ++-- .../components/transmission/sensor.py | 10 ++++++- 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 40e37ec4697..46ffd1196f7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -306,7 +306,7 @@ homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus homeassistant/components/tradfri/* @ggravlingen homeassistant/components/trafikverket_train/* @endor-force -homeassistant/components/transmission/* @engrbm87 +homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @robbiet480 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 6cfd6bf640a..be41ca85998 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -232,6 +232,7 @@ class TransmissionData: self._api = api self.completed_torrents = [] self.started_torrents = [] + self.started_torrent_dict = {} @property def host(self): @@ -250,6 +251,7 @@ class TransmissionData: self.torrents = self._api.get_torrents() self.session = self._api.get_session() + self.check_started_torrent_info() self.check_completed_torrent() self.check_started_torrent() _LOGGER.debug("Torrent Data for %s Updated", self.host) @@ -301,6 +303,31 @@ class TransmissionData: self.hass.bus.fire("transmission_started_torrent", {"name": var}) self.started_torrents = actual_started_torrents + def check_started_torrent_info(self): + """Get started torrent info functionality.""" + all_torrents = self._api.get_torrents() + current_down = {} + + for torrent in all_torrents: + if torrent.status == "downloading": + info = self.started_torrent_dict[torrent.name] = { + "added_date": torrent.addedDate, + "percent_done": f"{torrent.percentDone * 100:.2f}", + } + try: + info["eta"] = str(torrent.eta) + except ValueError: + info["eta"] = "unknown" + + current_down[torrent.name] = True + + elif torrent.name in self.started_torrent_dict: + self.started_torrent_dict.pop(torrent.name) + + for torrent in list(self.started_torrent_dict): + if torrent not in current_down: + self.started_torrent_dict.pop(torrent) + def get_started_torrent_count(self): """Get the number of started torrents.""" return len(self.started_torrents) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 472bb32a391..5540f718ba1 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -17,6 +17,7 @@ DEFAULT_NAME = "Transmission" DEFAULT_PORT = 9091 DEFAULT_SCAN_INTERVAL = 120 +STATE_ATTR_TORRENT_INFO = "torrent_info" ATTR_TORRENT = "torrent" SERVICE_ADD_TORRENT = "add_torrent" diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index c2fa31d7b50..9618a5677ad 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -8,6 +8,7 @@ ], "dependencies": [], "codeowners": [ - "@engrbm87" + "@engrbm87", + "@JPHutchins" ] -} \ No newline at end of file +} diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index d9fd2b51144..489582de157 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -6,7 +6,8 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DOMAIN, SENSOR_TYPES +from .const import DOMAIN, SENSOR_TYPES, STATE_ATTR_TORRENT_INFO + _LOGGER = logging.getLogger(__name__) @@ -82,6 +83,13 @@ class TransmissionSensor(Entity): """Could the device be accessed during the last update call.""" return self._tm_client.api.available + @property + def device_state_attributes(self): + """Return the state attributes, if any.""" + if self._tm_client.api.started_torrent_dict and self.type == "started_torrents": + return {STATE_ATTR_TORRENT_INFO: self._tm_client.api.started_torrent_dict} + return None + async def async_added_to_hass(self): """Handle entity which will be added.""" async_dispatcher_connect( From 7887850505bbaef83467c732e07b23236e31f1d2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 28 Oct 2019 14:34:13 +0100 Subject: [PATCH 054/306] More header cleanup for websocket proxy (#28288) --- homeassistant/components/hassio/ingress.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4ecb9a8419f..3b6fee56354 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -211,6 +211,10 @@ def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE, hdrs.CONTENT_ENCODING, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, ): continue headers[name] = value From c1d88dd7a42c1b013cee43f1cb1ec03cebbba595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 28 Oct 2019 14:47:04 +0100 Subject: [PATCH 055/306] Bump avea to 1.4 (#28287) * Bump avea to 1.4 * Bump avea to 1.4 #2 --- homeassistant/components/avea/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index 4fb9ab9f420..f6217eeed18 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/avea", "dependencies": [], "codeowners": ["@pattyland"], - "requirements": ["avea==1.2.8"] -} \ No newline at end of file + "requirements": ["avea==1.4"] +} diff --git a/requirements_all.txt b/requirements_all.txt index b589ef6d918..e30f50f7e14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ aurorapy==0.2.6 av==6.1.2 # homeassistant.components.avea -avea==1.2.8 +avea==1.4 # homeassistant.components.avion # avion==0.10 From 30f4ee121a0d07e74452fa1ffff413574cd23bd1 Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Mon, 28 Oct 2019 14:54:42 +0100 Subject: [PATCH 056/306] Remove GTT component (#28286) * removed GTT component * Removed gtt.py from coveragerc --- .coveragerc | 1 - homeassistant/components/gtt/__init__.py | 1 - homeassistant/components/gtt/manifest.json | 10 -- homeassistant/components/gtt/sensor.py | 114 --------------------- requirements_all.txt | 3 - 5 files changed, 129 deletions(-) delete mode 100644 homeassistant/components/gtt/__init__.py delete mode 100644 homeassistant/components/gtt/manifest.json delete mode 100644 homeassistant/components/gtt/sensor.py diff --git a/.coveragerc b/.coveragerc index 90efc417e03..e6f09d60eff 100644 --- a/.coveragerc +++ b/.coveragerc @@ -274,7 +274,6 @@ omit = homeassistant/components/growatt_server/sensor.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py - homeassistant/components/gtt/sensor.py homeassistant/components/habitica/* homeassistant/components/hangouts/* homeassistant/components/hangouts/__init__.py diff --git a/homeassistant/components/gtt/__init__.py b/homeassistant/components/gtt/__init__.py deleted file mode 100644 index cbb508154dd..00000000000 --- a/homeassistant/components/gtt/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The gtt component.""" diff --git a/homeassistant/components/gtt/manifest.json b/homeassistant/components/gtt/manifest.json deleted file mode 100644 index 217b1755554..00000000000 --- a/homeassistant/components/gtt/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "gtt", - "name": "Gtt", - "documentation": "https://www.home-assistant.io/integrations/gtt", - "requirements": [ - "pygtt==1.1.2" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/gtt/sensor.py b/homeassistant/components/gtt/sensor.py deleted file mode 100644 index cd66a670696..00000000000 --- a/homeassistant/components/gtt/sensor.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Sensor to get GTT's timetable for a stop.""" -import logging -from datetime import timedelta, datetime - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import DEVICE_CLASS_TIMESTAMP -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_STOP = "stop" -CONF_BUS_NAME = "bus_name" - -ICON = "mdi:train" - -SCAN_INTERVAL = timedelta(minutes=2) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_STOP): cv.string, vol.Optional(CONF_BUS_NAME): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Gtt platform.""" - stop = config[CONF_STOP] - bus_name = config.get(CONF_BUS_NAME) - - add_entities([GttSensor(stop, bus_name)], True) - - -class GttSensor(Entity): - """Representation of a Gtt Sensor.""" - - def __init__(self, stop, bus_name): - """Initialize the Gtt sensor.""" - self.data = GttData(stop, bus_name) - self._state = None - self._name = f"Stop {stop}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon of the sensor.""" - return ICON - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - attr = {"bus_name": self.data.state_bus["bus_name"]} - return attr - - def update(self): - """Update device state.""" - self.data.get_data() - next_time = get_datetime(self.data.state_bus) - self._state = next_time.isoformat() - - -class GttData: - """Inteface to PyGTT.""" - - def __init__(self, stop, bus_name): - """Initialize the GttData class.""" - from pygtt import PyGTT - - self._pygtt = PyGTT() - self._stop = stop - self._bus_name = bus_name - self.bus_list = {} - self.state_bus = {} - - def get_data(self): - """Get the data from the api.""" - self.bus_list = self._pygtt.get_by_stop(self._stop) - self.bus_list.sort(key=get_datetime) - - if self._bus_name is not None: - self.state_bus = self.get_bus_by_name() - return - - self.state_bus = self.bus_list[0] - - def get_bus_by_name(self): - """Get the bus by name.""" - for bus in self.bus_list: - if bus["bus_name"] == self._bus_name: - return bus - - -def get_datetime(bus): - """Get the datetime from a bus.""" - bustime = datetime.strptime(bus["time"][0]["run"], "%H:%M") - now = datetime.now() - bustime = bustime.replace(year=now.year, month=now.month, day=now.day) - if bustime < now: - bustime = bustime + timedelta(days=1) - return bustime diff --git a/requirements_all.txt b/requirements_all.txt index e30f50f7e14..1cbb3d33f38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1222,9 +1222,6 @@ pygogogate2==0.1.1 # homeassistant.components.gtfs pygtfs==0.1.5 -# homeassistant.components.gtt -pygtt==1.1.2 - # homeassistant.components.version pyhaversion==3.1.0 From 549e8cf2c504942348f42ea5f293d2e8691f1d95 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 28 Oct 2019 16:45:08 +0100 Subject: [PATCH 057/306] Hue: Create new config flow when auth is lost (#28204) * Hue: Create new config flow when auth is lost * Fix tests * Fix tests * Comments * Lint --- homeassistant/components/hue/bridge.py | 23 +++++++++----- homeassistant/components/hue/helpers.py | 12 +++++++ homeassistant/components/hue/light.py | 18 ++++++++--- homeassistant/components/hue/sensor_base.py | 16 ++++++++-- tests/components/hue/test_bridge.py | 22 +++++++++++++ tests/components/hue/test_light.py | 5 +-- tests/components/hue/test_sensor_base.py | 35 +++++++++++++++++++-- 7 files changed, 112 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 6a654744397..5015ec669aa 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -5,12 +5,12 @@ import aiohue import async_timeout import voluptuous as vol -from homeassistant import config_entries from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER from .errors import AuthenticationRequired, CannotConnect +from .helpers import create_config_flow SERVICE_HUE_SCENE = "hue_activate_scene" ATTR_GROUP_NAME = "group_name" @@ -30,6 +30,7 @@ class HueBridge: self.allow_unreachable = allow_unreachable self.allow_groups = allow_groups self.available = True + self.authorized = False self.api = None @property @@ -49,13 +50,7 @@ class HueBridge: # 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. - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": host}, - ) - ) + create_config_flow(hass, host) return False except CannotConnect: @@ -82,6 +77,7 @@ class HueBridge: DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA ) + self.authorized = True return True async def async_reset(self): @@ -155,6 +151,17 @@ class HueBridge: await group.set_action(scene=scene.id) + async def handle_unauthorized_error(self): + """Create a new config flow when the authorization is no longer valid.""" + if not self.authorized: + # we already created a new config flow, no need to do it again + return + LOGGER.error( + "Unable to authorize to bridge %s, setup the linking again.", self.host + ) + self.authorized = False + create_config_flow(self.hass, self.host) + async def get_bridge(hass, host, username=None): """Create a bridge object and verify authentication.""" diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 971509ab647..af0f996b537 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -1,6 +1,7 @@ """Helper functions for Philips Hue.""" from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant import config_entries from .const import DOMAIN @@ -31,3 +32,14 @@ async def remove_devices(hass, config_entry, api_ids, current): for item_id in removed_items: del current[item_id] + + +def create_config_flow(hass, host): + """Start a config flow.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": host}, + ) + ) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 041eb76c1d3..d58e4608b65 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -189,6 +189,9 @@ async def async_update_items( progress_waiting, ): """Update either groups or lights from the bridge.""" + if not bridge.authorized: + return + if is_group: api_type = "group" api = bridge.api.groups @@ -200,6 +203,9 @@ async def async_update_items( start = monotonic() with async_timeout.timeout(4): await api.update() + except aiohue.Unauthorized: + await bridge.handle_unauthorized_error() + return except (asyncio.TimeoutError, aiohue.AiohueException) as err: _LOGGER.debug("Failed to fetch %s: %s", api_type, err) @@ -337,10 +343,14 @@ class HueLight(Light): @property def available(self): """Return if light is available.""" - return self.bridge.available and ( - self.is_group - or self.bridge.allow_unreachable - or self.light.state["reachable"] + return ( + self.bridge.available + and self.bridge.authorized + and ( + self.is_group + or self.bridge.allow_unreachable + or self.light.state["reachable"] + ) ) @property diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 7236dfbd886..62bd98df3a2 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from time import monotonic -from aiohue import AiohueException +from aiohue import AiohueException, Unauthorized from aiohue.sensors import TYPE_ZLL_PRESENCE import async_timeout @@ -80,6 +80,11 @@ class SensorManager: async def async_update_bridge(now): """Will update sensors from the bridge.""" + + # don't update when we are not authorized + if not self.bridge.authorized: + return + await self.async_update_items() async_track_point_in_utc_time( @@ -96,6 +101,9 @@ class SensorManager: start = monotonic() with async_timeout.timeout(4): await api.update() + except Unauthorized: + await self.bridge.handle_unauthorized_error() + return except (asyncio.TimeoutError, AiohueException) as err: _LOGGER.debug("Failed to fetch sensor: %s", err) @@ -220,8 +228,10 @@ class GenericHueSensor: @property def available(self): """Return if sensor is available.""" - return self.bridge.available and ( - self.bridge.allow_unreachable or self.sensor.config["reachable"] + return ( + self.bridge.available + and self.bridge.authorized + and (self.bridge.allow_unreachable or self.sensor.config["reachable"]) ) @property diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index a426d35abf7..7265b468714 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -91,3 +91,25 @@ async def test_reset_unloads_entry_if_setup(): assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 3 assert len(hass.services.async_remove.mock_calls) == 1 + + +async def test_handle_unauthorized(): + """Test handling an unauthorized error on update.""" + hass = Mock() + entry = Mock() + entry.data = {"host": "1.2.3.4", "username": "mock-username"} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())): + assert await hue_bridge.async_setup() is True + + assert hue_bridge.authorized is True + + await hue_bridge.handle_unauthorized_error() + + assert hue_bridge.authorized is False + assert len(hass.async_create_task.mock_calls) == 4 + assert len(hass.config_entries.flow.async_init.mock_calls) == 1 + assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == { + "host": "1.2.3.4" + } diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 582cc185bc8..88c527a50ca 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -180,6 +180,7 @@ def mock_bridge(hass): """Mock a Hue bridge.""" bridge = Mock( available=True, + authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), @@ -598,13 +599,13 @@ async def test_update_timeout(hass, mock_bridge): async def test_update_unauthorized(hass, mock_bridge): - """Test bridge marked as not available if unauthorized during update.""" + """Test bridge marked as not authorized if unauthorized during update.""" mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized) mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert mock_bridge.available is False + assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 async def test_light_turn_on_service(hass, mock_bridge): diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index 72ac816483a..ba259dccf71 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -256,6 +256,7 @@ def create_mock_bridge(): """Create a mock Hue bridge.""" bridge = Mock( available=True, + authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), @@ -425,6 +426,36 @@ async def test_new_sensor_discovered(hass, mock_bridge): assert temperature.state == "17.75" +async def test_sensor_removed(hass, mock_bridge): + """Test if 2nd update has removed sensor.""" + mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 6 + + mock_bridge.mock_sensor_responses.clear() + keys = ("1", "2", "3") + mock_bridge.mock_sensor_responses.append({k: SENSOR_RESPONSE[k] for k in keys}) + + # Force updates to run again + sm_key = hue_sensor_base.SENSOR_MANAGER_FORMAT.format("mock-host") + sm = hass.data[hue.DOMAIN][sm_key] + await sm.async_update_items() + + # To flush out the service call to update the group + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + sensor = hass.states.get("binary_sensor.living_room_sensor_motion") + assert sensor is not None + + removed_sensor = hass.states.get("binary_sensor.kitchen_sensor_motion") + assert removed_sensor is None + + async def test_update_timeout(hass, mock_bridge): """Test bridge marked as not available if timeout error during update.""" mock_bridge.api.sensors.update = Mock(side_effect=asyncio.TimeoutError) @@ -435,9 +466,9 @@ async def test_update_timeout(hass, mock_bridge): async def test_update_unauthorized(hass, mock_bridge): - """Test bridge marked as not available if unauthorized during update.""" + """Test bridge marked as not authorized if unauthorized during update.""" mock_bridge.api.sensors.update = Mock(side_effect=aiohue.Unauthorized) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert mock_bridge.available is False + assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 From 335872b54d520c3322619a1266045943824bb47e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 28 Oct 2019 16:54:15 +0100 Subject: [PATCH 058/306] Revert "More header cleanup for websocket proxy (#28288)" (#28293) This reverts commit 7887850505bbaef83467c732e07b23236e31f1d2. --- homeassistant/components/hassio/ingress.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 3b6fee56354..4ecb9a8419f 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -211,10 +211,6 @@ def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE, hdrs.CONTENT_ENCODING, - hdrs.SEC_WEBSOCKET_EXTENSIONS, - hdrs.SEC_WEBSOCKET_PROTOCOL, - hdrs.SEC_WEBSOCKET_VERSION, - hdrs.SEC_WEBSOCKET_KEY, ): continue headers[name] = value From 31dd69196c65a550808ad96e52f9923beb5a56e5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Oct 2019 12:39:37 -0500 Subject: [PATCH 059/306] Bump library to 0.0.3 (#28294) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 90ae305148e..8edccda75e0 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.0.6", "plexauth==0.0.5", - "plexwebsocket==0.0.1" + "plexwebsocket==0.0.3" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index 1cbb3d33f38..98c593fc6c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.1 +plexwebsocket==0.0.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 861e1098664..5987c3061b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.1 +plexwebsocket==0.0.3 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From f7a64019b60c738cbcfb736a1c4edafb330bf8c0 Mon Sep 17 00:00:00 2001 From: Yann Jajkiewicz Date: Mon, 28 Oct 2019 19:22:15 +0100 Subject: [PATCH 060/306] Add support for Somfy Garage door Rollixo IO DiscreteGarageOpenerIOComponent in Tahoma component (#28291) --- homeassistant/components/tahoma/__init__.py | 1 + homeassistant/components/tahoma/cover.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 6bcc783400c..9fc8ca3cf2e 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -47,6 +47,7 @@ TAHOMA_TYPES = { "io:VerticalExteriorAwningIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", "io:GarageOpenerIOComponent": "cover", + "io:DiscreteGarageOpenerIOComponent": "cover", "rtds:RTDSContactSensor": "sensor", "rtds:RTDSMotionSensor": "sensor", "rtds:RTDSSmokeSensor": "smoke", diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index 7448eb27ae0..6c5dcbd807c 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -37,6 +37,7 @@ TAHOMA_DEVICE_CLASSES = { "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, + "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, "rts:BlindRTSComponent": DEVICE_CLASS_BLIND, "rts:CurtainRTSComponent": DEVICE_CLASS_CURTAIN, "rts:DualCurtainRTSComponent": DEVICE_CLASS_CURTAIN, From f88ead597a8da0212d1f34e80b5f9ef1ee1b940a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 28 Oct 2019 22:36:26 +0200 Subject: [PATCH 061/306] Type hint improvements (#28260) * Add and improve core and config_entries type hints * Complete and improve config_entries type hints * More entity registry type hints * Complete helpers.event type hints --- homeassistant/components/group/light.py | 5 +- homeassistant/components/switch/light.py | 5 +- homeassistant/config_entries.py | 73 ++++++++++++------- homeassistant/core.py | 2 +- homeassistant/data_entry_flow.py | 6 +- .../helpers/config_entry_oauth2_flow.py | 2 +- homeassistant/helpers/entity_registry.py | 48 ++++++------ homeassistant/helpers/event.py | 70 +++++++++++------- homeassistant/helpers/template.py | 13 +++- 9 files changed, 135 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 85804552494..2cd65028131 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -16,7 +16,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import State, callback +from homeassistant.core import CALLBACK_TYPE, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -96,7 +96,7 @@ class LightGroup(light.Light): self._effect_list: Optional[List[str]] = None self._effect: Optional[str] = None self._supported_features: int = 0 - self._async_unsub_state_changed = None + self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -108,6 +108,7 @@ class LightGroup(light.Light): """Handle child updates.""" self.async_schedule_update_ha_state(True) + assert self.hass is not None self._async_unsub_state_changed = async_track_state_change( self.hass, self._entity_ids, async_state_changed_listener ) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index b0abf957991..1bdc1d39083 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -12,7 +12,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import State, callback +from homeassistant.core import CALLBACK_TYPE, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change @@ -56,7 +56,7 @@ class LightSwitch(Light): self._switch_entity_id = switch_entity_id self._is_on = False self._available = False - self._async_unsub_state_changed = None + self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None @property def name(self) -> str: @@ -113,6 +113,7 @@ class LightSwitch(Light): """Handle child updates.""" self.async_schedule_update_ha_state(True) + assert self.hass is not None self._async_unsub_state_changed = async_track_state_change( self.hass, self._switch_entity_id, async_state_changed_listener ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index aee15d6c0ce..ae7c534adf8 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,7 +3,7 @@ import asyncio import logging import functools import uuid -from typing import Any, Callable, List, Optional, Set +from typing import Any, Callable, Dict, List, Optional, Set, cast import weakref import attr @@ -14,11 +14,11 @@ from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry from homeassistant.helpers import entity_registry +from homeassistant.helpers.event import Event -# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) -_UNDEF = object() +_UNDEF: dict = {} SOURCE_USER = "user" SOURCE_DISCOVERY = "discovery" @@ -205,7 +205,7 @@ class ConfigEntry: wait_time, ) - async def setup_again(now): + async def setup_again(now: Any) -> None: """Run setup again.""" self._async_cancel_retry_setup = None await self.async_setup(hass, integration=integration, tries=tries) @@ -357,7 +357,7 @@ class ConfigEntry: return lambda: self.update_listeners.remove(weak_listener) - def as_dict(self): + def as_dict(self) -> Dict[str, Any]: """Return dictionary version of this entry.""" return { "entry_id": self.entry_id, @@ -418,7 +418,7 @@ class ConfigEntries: return list(self._entries) return [entry for entry in self._entries if entry.domain == domain] - async def async_remove(self, entry_id): + async def async_remove(self, entry_id: str) -> Dict[str, Any]: """Remove an entry.""" entry = self.async_get_entry(entry_id) @@ -529,8 +529,13 @@ class ConfigEntries: @callback def async_update_entry( - self, entry, *, data=_UNDEF, options=_UNDEF, system_options=_UNDEF - ): + self, + entry: ConfigEntry, + *, + data: dict = _UNDEF, + options: dict = _UNDEF, + system_options: dict = _UNDEF, + ) -> None: """Update a config entry.""" if data is not _UNDEF: entry.data = data @@ -547,7 +552,7 @@ class ConfigEntries: self._async_schedule_save() - async def async_forward_entry_setup(self, entry, domain): + async def async_forward_entry_setup(self, entry: ConfigEntry, domain: str) -> bool: """Forward the setup of an entry to a different component. By default an entry is setup with the component it belongs to. If that @@ -567,8 +572,9 @@ class ConfigEntries: integration = await loader.async_get_integration(self.hass, domain) await entry.async_setup(self.hass, integration=integration) + return True - async def async_forward_entry_unload(self, entry, domain): + async def async_forward_entry_unload(self, entry: ConfigEntry, domain: str) -> bool: """Forward the unloading of an entry to a different component.""" # It was never loaded. if domain not in self.hass.config.components: @@ -578,7 +584,9 @@ class ConfigEntries: return await entry.async_unload(self.hass, integration=integration) - async def _async_finish_flow(self, flow, result): + async def _async_finish_flow( + self, flow: "ConfigFlow", result: Dict[str, Any] + ) -> Dict[str, Any]: """Finish a config flow and add an entry.""" # Remove notification if no other discovery config entries in progress if not any( @@ -611,7 +619,9 @@ class ConfigEntries: result["result"] = entry return result - async def _async_create_flow(self, handler_key, *, context, data): + async def _async_create_flow( + self, handler_key: str, *, context: Dict[str, Any], data: Dict[str, Any] + ) -> "ConfigFlow": """Create a flow for specified handler. Handler key is the domain of the component that we want to set up. @@ -654,7 +664,7 @@ class ConfigEntries: notification_id=DISCOVERY_NOTIFICATION_ID, ) - flow = handler() + flow = cast(ConfigFlow, handler()) flow.init_step = source return flow @@ -663,12 +673,12 @@ class ConfigEntries: self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: """Return data to save.""" return {"entries": [entry.as_dict() for entry in self._entries]} -async def _old_conf_migrator(old_config): +async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]: """Migrate the pre-0.73 config format to the latest version.""" return {"entries": old_config} @@ -686,18 +696,20 @@ class ConfigFlow(data_entry_flow.FlowHandler): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow": """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler @callback - def _async_current_entries(self): + def _async_current_entries(self) -> List[ConfigEntry]: """Return current entries.""" + assert self.hass is not None return self.hass.config_entries.async_entries(self.handler) @callback - def _async_in_progress(self): + def _async_in_progress(self) -> List[Dict]: """Return other in progress flows for current domain.""" + assert self.hass is not None return [ flw for flw in self.hass.config_entries.flow.async_progress() @@ -715,29 +727,33 @@ class OptionsFlowManager: hass, self._async_create_flow, self._async_finish_flow ) - async def _async_create_flow(self, entry_id, *, context, data): + async def _async_create_flow( + self, entry_id: str, *, context: Dict[str, Any], data: Dict[str, Any] + ) -> Optional["OptionsFlow"]: """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 + return None if entry.domain not in HANDLERS: raise data_entry_flow.UnknownHandler - flow = HANDLERS[entry.domain].async_get_options_flow(entry) + flow = cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) return flow - async def _async_finish_flow(self, flow, result): + async def _async_finish_flow( + self, flow: "OptionsFlow", result: Dict[str, Any] + ) -> Optional[Dict[str, Any]]: """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 + return None self.hass.config_entries.async_update_entry(entry, options=result["data"]) result["result"] = True @@ -747,7 +763,7 @@ class OptionsFlowManager: class OptionsFlow(data_entry_flow.FlowHandler): """Base class for config option flows.""" - pass + handler: str @attr.s(slots=True) @@ -756,11 +772,11 @@ class SystemOptions: disable_new_entities = attr.ib(type=bool, default=False) - def update(self, *, disable_new_entities): + def update(self, *, disable_new_entities: bool) -> None: """Update properties.""" self.disable_new_entities = disable_new_entities - def as_dict(self): + def as_dict(self) -> Dict[str, Any]: """Return dictionary version of this config entrys system options.""" return {"disable_new_entities": self.disable_new_entities} @@ -784,7 +800,7 @@ class EntityRegistryDisabledHandler: entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entry_updated ) - async def _handle_entry_updated(self, event): + async def _handle_entry_updated(self, event: Event) -> None: """Handle entity registry entry update.""" if ( event.data["action"] != "update" @@ -811,6 +827,7 @@ class EntityRegistryDisabledHandler: config_entry = self.hass.config_entries.async_get_entry( entity_entry.config_entry_id ) + assert config_entry is not None if config_entry.entry_id not in self.changed and await support_entry_unload( self.hass, config_entry.domain @@ -830,7 +847,7 @@ class EntityRegistryDisabledHandler: self.RELOAD_AFTER_UPDATE_DELAY, self._handle_reload ) - async def _handle_reload(self, _now): + async def _handle_reload(self, _now: Any) -> None: """Handle a reload.""" self._remove_call_later = None to_reload = self.changed diff --git a/homeassistant/core.py b/homeassistant/core.py index ec11b14edaa..01c5561d939 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1283,7 +1283,7 @@ class Config: self.skip_pip: bool = False # List of loaded components - self.components: set = set() + self.components: Set[str] = set() # API (HTTP) server configuration, see components.http.ApiConfig self.api: Optional[Any] = None diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index c06c69d9213..58d8e4ea131 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,6 +1,6 @@ """Classes to help gather user submissions.""" import logging -from typing import Dict, Any, Callable, Hashable, List, Optional +from typing import Dict, Any, Callable, List, Optional import uuid import voluptuous as vol from .core import callback, HomeAssistant @@ -58,7 +58,7 @@ class FlowManager: ] async def async_init( - self, handler: Hashable, *, context: Optional[Dict] = None, data: Any = None + self, handler: str, *, context: Optional[Dict] = None, data: Any = None ) -> Any: """Start a configuration flow.""" if context is None: @@ -170,7 +170,7 @@ class FlowHandler: # Set by flow manager flow_id: str = None # type: ignore hass: Optional[HomeAssistant] = None - handler: Optional[Hashable] = None + handler: Optional[str] = None cur_step: Optional[Dict[str, str]] = None context: Dict diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index d3db8febcb2..87832f60739 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -399,7 +399,7 @@ class OAuth2Session: new_token = await self.implementation.async_refresh_token(token) - self.hass.config_entries.async_update_entry( # type: ignore + self.hass.config_entries.async_update_entry( self.config_entry, data={**self.config_entry.data, "token": new_token} ) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 00671e9c776..08f29a9fb3e 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -7,15 +7,15 @@ The Entity Registry will persist itself 10 seconds after a new entity is registered. Registering a new entity while a timer is in progress resets the timer. """ -from asyncio import Event +import asyncio from collections import OrderedDict from itertools import chain import logging -from typing import List, Optional, cast +from typing import Any, Dict, Iterable, List, Optional, cast import attr -from homeassistant.core import callback, split_entity_id, valid_entity_id +from homeassistant.core import Event, callback, split_entity_id, valid_entity_id from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass from homeassistant.util import ensure_unique_string, slugify @@ -24,8 +24,7 @@ from homeassistant.util.yaml import load_yaml from .typing import HomeAssistantType -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any +# mypy: allow-untyped-defs, no-check-untyped-defs PATH_REGISTRY = "entity_registry.yaml" DATA_REGISTRY = "entity_registry" @@ -51,7 +50,7 @@ class RegistryEntry: platform = attr.ib(type=str) name = attr.ib(type=str, default=None) device_id = attr.ib(type=str, default=None) - config_entry_id = attr.ib(type=str, default=None) + config_entry_id: Optional[str] = attr.ib(default=None) disabled_by = attr.ib( type=Optional[str], default=None, @@ -68,12 +67,12 @@ class RegistryEntry: domain = attr.ib(type=str, init=False, repr=False) @domain.default - def _domain_default(self): + def _domain_default(self) -> str: """Compute domain value.""" return split_entity_id(self.entity_id)[0] @property - def disabled(self): + def disabled(self) -> bool: """Return if entry is disabled.""" return self.disabled_by is not None @@ -81,17 +80,17 @@ class RegistryEntry: class EntityRegistry: """Class to hold a registry of entities.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistantType): """Initialize the registry.""" self.hass = hass - self.entities = None + self.entities: Dict[str, RegistryEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_removed ) @callback - def async_is_registered(self, entity_id): + def async_is_registered(self, entity_id: str) -> bool: """Check if an entity_id is currently registered.""" return entity_id in self.entities @@ -116,8 +115,11 @@ class EntityRegistry: @callback def async_generate_entity_id( - self, domain, suggested_object_id, known_object_ids=None - ): + self, + domain: str, + suggested_object_id: str, + known_object_ids: Optional[Iterable[str]] = None, + ) -> str: """Generate an entity ID that does not conflict. Conflicts checked against registered and currently existing entities. @@ -195,7 +197,7 @@ class EntityRegistry: return entity @callback - def async_remove(self, entity_id): + def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" self.entities.pop(entity_id) self.hass.bus.async_fire( @@ -204,7 +206,7 @@ class EntityRegistry: self.async_schedule_save() @callback - def async_device_removed(self, event): + def async_device_removed(self, event: Event) -> None: """Handle the removal of a device. Remove entities from the registry that are associated to a device when @@ -309,7 +311,7 @@ class EntityRegistry: return new - async def async_load(self): + async def async_load(self) -> None: """Load the entity registry.""" data = await self.hass.helpers.storage.async_migrator( self.hass.config.path(PATH_REGISTRY), @@ -317,7 +319,7 @@ class EntityRegistry: old_conf_load_func=load_yaml, old_conf_migrate_func=_async_migrate, ) - entities = OrderedDict() + entities: Dict[str, RegistryEntry] = OrderedDict() if data is not None: for entity in data["entities"]: @@ -334,12 +336,12 @@ class EntityRegistry: self.entities = entities @callback - def async_schedule_save(self): + def async_schedule_save(self) -> None: """Schedule saving the entity registry.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> Dict[str, Any]: """Return data of entity registry to store in a file.""" data = {} @@ -359,7 +361,7 @@ class EntityRegistry: return data @callback - def async_clear_config_entry(self, config_entry): + def async_clear_config_entry(self, config_entry: str) -> None: """Clear config entry from registry entries.""" for entity_id in [ entity_id @@ -375,7 +377,7 @@ async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: reg_or_evt = hass.data.get(DATA_REGISTRY) if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = Event() + evt = hass.data[DATA_REGISTRY] = asyncio.Event() reg = EntityRegistry(hass) await reg.async_load() @@ -384,7 +386,7 @@ async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: evt.set() return reg - if isinstance(reg_or_evt, Event): + if isinstance(reg_or_evt, asyncio.Event): evt = reg_or_evt await evt.wait() return cast(EntityRegistry, hass.data.get(DATA_REGISTRY)) @@ -402,7 +404,7 @@ def async_entries_for_device( ] -async def _async_migrate(entities): +async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]: """Migrate the YAML config file to storage helper format.""" return { "entities": [ diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e819da9873a..715344a3969 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,13 +1,14 @@ """Helpers for listening to events.""" from datetime import datetime, timedelta import functools as ft -from typing import Any, Callable, Iterable, Optional, Union +from typing import Any, Callable, Dict, Iterable, Optional, Union, cast import attr from homeassistant.loader import bind_hass from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event +from homeassistant.helpers.template import Template +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event, State from homeassistant.const import ( ATTR_NOW, EVENT_STATE_CHANGED, @@ -21,16 +22,15 @@ from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name -def threaded_listener_factory(async_factory): +def threaded_listener_factory(async_factory: Callable[..., Any]) -> CALLBACK_TYPE: """Convert an async event helper to a threaded one.""" @ft.wraps(async_factory) - def factory(*args, **kwargs): + def factory(*args: Any, **kwargs: Any) -> CALLBACK_TYPE: """Call async event helper safely.""" hass = args[0] @@ -41,7 +41,7 @@ def threaded_listener_factory(async_factory): hass.loop, ft.partial(async_factory, *args, **kwargs) ).result() - def remove(): + def remove() -> None: """Threadsafe removal.""" run_callback_threadsafe(hass.loop, async_remove).result() @@ -52,7 +52,13 @@ def threaded_listener_factory(async_factory): @callback @bind_hass -def async_track_state_change(hass, entity_ids, action, from_state=None, to_state=None): +def async_track_state_change( + hass: HomeAssistant, + entity_ids: Union[str, Iterable[str]], + action: Callable[[str, State, State], None], + from_state: Union[None, str, Iterable[str]] = None, + to_state: Union[None, str, Iterable[str]] = None, +) -> CALLBACK_TYPE: """Track specific state changes. entity_ids, from_state and to_state can be string or list. @@ -74,9 +80,12 @@ def async_track_state_change(hass, entity_ids, action, from_state=None, to_state entity_ids = tuple(entity_id.lower() for entity_id in entity_ids) @callback - def state_change_listener(event): + def state_change_listener(event: Event) -> None: """Handle specific state changes.""" - if entity_ids != MATCH_ALL and event.data.get("entity_id") not in entity_ids: + if ( + entity_ids != MATCH_ALL + and cast(str, event.data.get("entity_id")) not in entity_ids + ): return old_state = event.data.get("old_state") @@ -103,7 +112,12 @@ track_state_change = threaded_listener_factory(async_track_state_change) @callback @bind_hass -def async_track_template(hass, template, action, variables=None): +def async_track_template( + hass: HomeAssistant, + template: Template, + action: Callable[[str, State, State], None], + variables: Optional[Dict[str, Any]] = None, +) -> CALLBACK_TYPE: """Add a listener that track state changes with template condition.""" from . import condition @@ -111,7 +125,7 @@ def async_track_template(hass, template, action, variables=None): already_triggered = False @callback - def template_condition_listener(entity_id, from_s, to_s): + def template_condition_listener(entity_id: str, from_s: State, to_s: State) -> None: """Check if condition is correct and run action.""" nonlocal already_triggered template_result = condition.async_template(hass, template, variables) @@ -134,18 +148,22 @@ track_template = threaded_listener_factory(async_track_template) @callback @bind_hass def async_track_same_state( - hass, period, action, async_check_same_func, entity_ids=MATCH_ALL -): + hass: HomeAssistant, + period: timedelta, + action: Callable[..., None], + async_check_same_func: Callable[[str, State, State], bool], + entity_ids: Union[str, Iterable[str]] = MATCH_ALL, +) -> CALLBACK_TYPE: """Track the state of entities for a period and run an action. If async_check_func is None it use the state of orig_value. Without entity_ids we track all state changes. """ - async_remove_state_for_cancel = None - async_remove_state_for_listener = None + async_remove_state_for_cancel: Optional[CALLBACK_TYPE] = None + async_remove_state_for_listener: Optional[CALLBACK_TYPE] = None @callback - def clear_listener(): + def clear_listener() -> None: """Clear all unsub listener.""" nonlocal async_remove_state_for_cancel, async_remove_state_for_listener @@ -157,7 +175,7 @@ def async_track_same_state( async_remove_state_for_cancel = None @callback - def state_for_listener(now): + def state_for_listener(now: Any) -> None: """Fire on state changes after a delay and calls action.""" nonlocal async_remove_state_for_listener async_remove_state_for_listener = None @@ -165,7 +183,9 @@ def async_track_same_state( hass.async_run_job(action) @callback - def state_for_cancel_listener(entity, from_state, to_state): + def state_for_cancel_listener( + entity: str, from_state: State, to_state: State + ) -> None: """Fire on changes and cancel for listener if changed.""" if not async_check_same_func(entity, from_state, to_state): clear_listener() @@ -193,7 +213,7 @@ def async_track_point_in_time( utc_point_in_time = dt_util.as_utc(point_in_time) @callback - def utc_converter(utc_now): + def utc_converter(utc_now: datetime) -> None: """Convert passed in UTC now to local now.""" hass.async_run_job(action, dt_util.as_local(utc_now)) @@ -213,7 +233,7 @@ def async_track_point_in_utc_time( point_in_time = dt_util.as_utc(point_in_time) @callback - def point_in_time_listener(event): + def point_in_time_listener(event: Event) -> None: """Listen for matching time_changed events.""" now = event.data[ATTR_NOW] @@ -225,7 +245,7 @@ def async_track_point_in_utc_time( # available to execute this listener it might occur that the # listener gets lined up twice to be executed. This will make # sure the second time it does nothing. - point_in_time_listener.run = True + setattr(point_in_time_listener, "run", True) async_unsub() hass.async_run_job(action, now) @@ -260,12 +280,12 @@ def async_track_time_interval( """Add a listener that fires repetitively at every timedelta interval.""" remove = None - def next_interval(): + def next_interval() -> datetime: """Return the next interval.""" return dt_util.utcnow() + interval @callback - def interval_listener(now): + def interval_listener(now: datetime) -> None: """Handle elapsed intervals.""" nonlocal remove remove = async_track_point_in_utc_time(hass, interval_listener, next_interval()) @@ -273,7 +293,7 @@ def async_track_time_interval( remove = async_track_point_in_utc_time(hass, interval_listener, next_interval()) - def remove_listener(): + def remove_listener() -> None: """Remove interval listener.""" remove() @@ -387,7 +407,7 @@ def async_track_utc_time_change( if all(val is None for val in (hour, minute, second)): @callback - def time_change_listener(event): + def time_change_listener(event: Event) -> None: """Fire every time event that comes in.""" hass.async_run_job(action, event.data[ATTR_NOW]) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1d9ca691451..aa17b2a1fba 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -7,7 +7,7 @@ import random import re from datetime import datetime from functools import wraps -from typing import Any, Iterable +from typing import Any, Dict, Iterable, List, Optional, Union import jinja2 from jinja2 import contextfilter, contextfunction @@ -72,7 +72,9 @@ def render_complex(value, variables=None): return value.async_render(variables) -def extract_entities(template, variables=None): +def extract_entities( + template: Optional[str], variables: Optional[Dict[str, Any]] = None +) -> Union[str, List[str]]: """Extract all entities for state_changed listener from template string.""" if template is None or _RE_JINJA_DELIMITERS.search(template) is None: return [] @@ -86,6 +88,7 @@ def extract_entities(template, variables=None): for result in extraction: if ( result[0] == "trigger.entity_id" + and variables and "trigger" in variables and "entity_id" in variables["trigger"] ): @@ -163,7 +166,7 @@ class Template: if not isinstance(template, str): raise TypeError("Expected template to be a string") - self.template = template + self.template: str = template self._compiled_code = None self._compiled = None self.hass = hass @@ -187,7 +190,9 @@ class Template: except jinja2.exceptions.TemplateSyntaxError as err: raise TemplateError(err) - def extract_entities(self, variables=None): + def extract_entities( + self, variables: Dict[str, Any] = None + ) -> Union[str, List[str]]: """Extract all entities for state_changed listener.""" return extract_entities(self.template, variables) From 0ef99934b70c7c8f27bacf9dc985e12a9705dab5 Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Mon, 28 Oct 2019 16:42:06 -0700 Subject: [PATCH 062/306] Add more iaqualink entity properties, fix timeout issues (#28236) * iaqualink: implement some more entity properties * Style fixes --- .../components/iaqualink/__init__.py | 27 ++++++++++++++++--- .../components/iaqualink/manifest.json | 8 ++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 9ce0e04895f..c3fa2bb1eb8 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -2,8 +2,8 @@ import asyncio from functools import wraps import logging +from typing import Any, Dict -from aiohttp import ClientTimeout import voluptuous as vol from iaqualink import ( @@ -26,7 +26,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] - session = async_create_clientsession(hass, timeout=ClientTimeout(total=5)) + session = async_get_clientsession(hass) aqualink = AqualinkClient(username, password, session) try: await aqualink.login() @@ -210,3 +210,24 @@ class AqualinkEntity(Entity): def unique_id(self) -> str: """Return a unique identifier for this entity.""" return f"{self.dev.system.serial}_{self.dev.name}" + + @property + def assumed_state(self) -> bool: + """Return whether the state is based on actual reading from the device.""" + return not self.dev.system.last_run_success + + @property + def available(self) -> bool: + """Return whether the device is available or not.""" + return self.dev.system.online + + @property + def device_info(self) -> Dict[str, Any]: + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "model": self.dev.__class__.__name__.replace("Aqualink", ""), + "manufacturer": "Jandy", + "via_device": (DOMAIN, self.dev.system.serial), + } diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index e883aec371c..85392e6371b 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,10 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "dependencies": [], - "codeowners": [ - "@flz" - ], - "requirements": [ - "iaqualink==0.2.9" - ] + "codeowners": ["@flz"], + "requirements": ["iaqualink==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 98c593fc6c0..5679d126e2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -676,7 +676,7 @@ hydrawiser==0.1.1 # i2csense==0.0.4 # homeassistant.components.iaqualink -iaqualink==0.2.9 +iaqualink==0.3.0 # homeassistant.components.watson_tts ibm-watson==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5987c3061b4..7a13a44288a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -262,7 +262,7 @@ httplib2==0.10.3 huawei-lte-api==1.4.3 # homeassistant.components.iaqualink -iaqualink==0.2.9 +iaqualink==0.3.0 # homeassistant.components.influxdb influxdb==5.2.3 From 1e27a1f2b901474523a86218a9b3eee56cc127e3 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Tue, 29 Oct 2019 00:59:13 +0100 Subject: [PATCH 063/306] Add keyboard_remote trigger on multiple event types and emulate key hold events (#27761) * convert keyboard_remote to async and add possibility to trigger on multiple event types, as well as emulate key hold events * update requirements * cleanup shutdown handling and config handling as well as address other minor comments * cleanup unused return values and debug message formatting * move start and stop event listen to separate coroutine plus minor cleanup * make setup coroutine a function * fix import order and attribute defined outside of init * add to codeowners * update codeowners --- CODEOWNERS | 1 + .../components/keyboard_remote/__init__.py | 407 +++++++++++------- .../components/keyboard_remote/manifest.json | 4 +- requirements_all.txt | 5 +- 4 files changed, 260 insertions(+), 157 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 46ffd1196f7..dac59039935 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -158,6 +158,7 @@ homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel +homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 8b901dcc61e..d4ed6128cbe 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,12 +1,11 @@ """Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error -import threading import logging -import os -import time +import asyncio +from evdev import InputDevice, categorize, ecodes, list_devices +import aionotify import voluptuous as vol - import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP @@ -26,6 +25,11 @@ KEYBOARD_REMOTE_CONNECTED = "keyboard_remote_connected" KEYBOARD_REMOTE_DISCONNECTED = "keyboard_remote_disconnected" TYPE = "type" +EMULATE_KEY_HOLD = "emulate_key_hold" +EMULATE_KEY_HOLD_DELAY = "emulate_key_hold_delay" +EMULATE_KEY_HOLD_REPEAT = "emulate_key_hold_repeat" + +DEVINPUT = "/dev/input" CONFIG_SCHEMA = vol.Schema( { @@ -36,11 +40,15 @@ CONFIG_SCHEMA = vol.Schema( { vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string, vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string, - vol.Optional(TYPE, default="key_up"): vol.All( - cv.string, vol.Any("key_up", "key_down", "key_hold") + vol.Optional(TYPE, default=["key_up"]): vol.All( + cv.ensure_list, [vol.In(KEY_VALUE)] ), + vol.Optional(EMULATE_KEY_HOLD, default=False): cv.boolean, + vol.Optional(EMULATE_KEY_HOLD_DELAY, default=0.250): float, + vol.Optional(EMULATE_KEY_HOLD_REPEAT, default=0.033): float, } - ) + ), + cv.has_at_least_one_key(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP), ], ) }, @@ -48,165 +56,256 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up the keyboard_remote.""" config = config.get(DOMAIN) - keyboard_remote = KeyboardRemote(hass, config) - - def _start_keyboard_remote(_event): - keyboard_remote.run() - - def _stop_keyboard_remote(_event): - keyboard_remote.stop() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_keyboard_remote) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_keyboard_remote) + remote = KeyboardRemote(hass, config) + remote.setup() return True -class KeyboardRemoteThread(threading.Thread): - """This interfaces with the inputdevice using evdev.""" - - def __init__(self, hass, device_name, device_descriptor, key_value): - """Construct a thread listening for events on one device.""" - self.hass = hass - self.device_name = device_name - self.device_descriptor = device_descriptor - self.key_value = key_value - - if self.device_descriptor: - self.device_id = self.device_descriptor - else: - self.device_id = self.device_name - - self.dev = self._get_keyboard_device() - if self.dev is not None: - _LOGGER.debug("Keyboard connected, %s", self.device_id) - else: - _LOGGER.debug( - "Keyboard not connected, %s. " "Check /dev/input/event* permissions", - self.device_id, - ) - - id_folder = "/dev/input/by-id/" - - if os.path.isdir(id_folder): - from evdev import InputDevice, list_devices - - device_names = [ - InputDevice(file_name).name for file_name in list_devices() - ] - _LOGGER.debug( - "Possible device names are: %s. " - "Possible device descriptors are %s: %s", - device_names, - id_folder, - os.listdir(id_folder), - ) - - threading.Thread.__init__(self) - self.stopped = threading.Event() - self.hass = hass - - def _get_keyboard_device(self): - """Get the keyboard device.""" - from evdev import InputDevice, list_devices - - if self.device_name: - devices = [InputDevice(file_name) for file_name in list_devices()] - for device in devices: - if self.device_name == device.name: - return device - elif self.device_descriptor: - try: - device = InputDevice(self.device_descriptor) - except OSError: - pass - else: - return device - return None - - def run(self): - """Run the loop of the KeyboardRemote.""" - from evdev import categorize, ecodes - - if self.dev is not None: - self.dev.grab() - _LOGGER.debug("Interface started for %s", self.dev) - - while not self.stopped.isSet(): - # Sleeps to ease load on processor - time.sleep(0.05) - - if self.dev is None: - self.dev = self._get_keyboard_device() - if self.dev is not None: - self.dev.grab() - self.hass.bus.fire( - KEYBOARD_REMOTE_CONNECTED, - { - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name, - }, - ) - _LOGGER.debug("Keyboard re-connected, %s", self.device_id) - else: - continue - - try: - event = self.dev.read_one() - except OSError: # Keyboard Disconnected - self.dev = None - self.hass.bus.fire( - KEYBOARD_REMOTE_DISCONNECTED, - { - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name, - }, - ) - _LOGGER.debug("Keyboard disconnected, %s", self.device_id) - continue - - if not event: - continue - - if event.type is ecodes.EV_KEY and event.value is self.key_value: - _LOGGER.debug(categorize(event)) - self.hass.bus.fire( - KEYBOARD_REMOTE_COMMAND_RECEIVED, - { - KEY_CODE: event.code, - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name, - }, - ) - - class KeyboardRemote: - """Sets up one thread per device.""" + """Manage device connection/disconnection using inotify to asynchronously monitor.""" def __init__(self, hass, config): - """Construct a KeyboardRemote interface object.""" - self.threads = [] + """Create handlers and setup dictionaries to keep track of them.""" + self.hass = hass + self.handlers_by_name = {} + self.handlers_by_descriptor = {} + self.active_handlers_by_descriptor = {} + self.watcher = None + self.monitor_task = None + for dev_block in config: - device_descriptor = dev_block.get(DEVICE_DESCRIPTOR) - device_name = dev_block.get(DEVICE_NAME) - key_value = KEY_VALUE.get(dev_block.get(TYPE, "key_up")) + handler = self.DeviceHandler(hass, dev_block) + descriptor = dev_block.get(DEVICE_DESCRIPTOR) + if descriptor is not None: + self.handlers_by_descriptor[descriptor] = handler + else: + name = dev_block.get(DEVICE_NAME) + self.handlers_by_name[name] = handler - if device_descriptor is not None or device_name is not None: - thread = KeyboardRemoteThread( - hass, device_name, device_descriptor, key_value + def setup(self): + """Listen for Home Assistant start and stop events.""" + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self.async_start_monitoring + ) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.async_stop_monitoring + ) + + async def async_start_monitoring(self, event): + """Start monitoring of events and devices. + + Start inotify watching for events, start event monitoring for those already + connected, and start monitoring for device connection/disconnection. + """ + + # start watching + self.watcher = aionotify.Watcher() + self.watcher.watch( + alias="devinput", + path=DEVINPUT, + flags=aionotify.Flags.CREATE + | aionotify.Flags.ATTRIB + | aionotify.Flags.DELETE, + ) + await self.watcher.setup(self.hass.loop) + + # add initial devices (do this AFTER starting watcher in order to + # avoid race conditions leading to missing device connections) + initial_start_monitoring = set() + descriptors = list_devices(DEVINPUT) + for descriptor in descriptors: + dev, handler = self.get_device_handler(descriptor) + + if handler is None: + continue + + self.active_handlers_by_descriptor[descriptor] = handler + initial_start_monitoring.add(handler.async_start_monitoring(dev)) + + if initial_start_monitoring: + await asyncio.wait(initial_start_monitoring) + + self.monitor_task = self.hass.async_create_task(self.async_monitor_devices()) + + async def async_stop_monitoring(self, event): + """Stop and cleanup running monitoring tasks.""" + + _LOGGER.debug("Cleanup on shutdown") + + if self.monitor_task is not None: + if not self.monitor_task.done(): + self.monitor_task.cancel() + await self.monitor_task + + handler_stop_monitoring = set() + for handler in self.active_handlers_by_descriptor.values(): + handler_stop_monitoring.add(handler.async_stop_monitoring()) + + if handler_stop_monitoring: + await asyncio.wait(handler_stop_monitoring) + + def get_device_handler(self, descriptor): + """Find the correct device handler given a descriptor (path).""" + + # devices are often added and then correct permissions set after + try: + dev = InputDevice(descriptor) + except (OSError, PermissionError): + return (None, None) + + handler = None + if descriptor in self.handlers_by_descriptor: + handler = self.handlers_by_descriptor[descriptor] + elif dev.name in self.handlers_by_name: + handler = self.handlers_by_name[dev.name] + + return (dev, handler) + + async def async_monitor_devices(self): + """Monitor asynchronously for device connection/disconnection or permissions changes.""" + + try: + while True: + event = await self.watcher.get_event() + descriptor = f"{DEVINPUT}/{event.name}" + + descriptor_active = descriptor in self.active_handlers_by_descriptor + + if (event.flags & aionotify.Flags.DELETE) and descriptor_active: + handler = self.active_handlers_by_descriptor[descriptor] + del self.active_handlers_by_descriptor[descriptor] + await handler.async_stop_monitoring() + elif ( + (event.flags & aionotify.Flags.CREATE) + or (event.flags & aionotify.Flags.ATTRIB) + ) and not descriptor_active: + dev, handler = self.get_device_handler(descriptor) + if handler is None: + continue + self.active_handlers_by_descriptor[descriptor] = handler + await handler.async_start_monitoring(dev) + except asyncio.CancelledError: + return + + class DeviceHandler: + """Manage input events using evdev with asyncio.""" + + def __init__(self, hass, dev_block): + """Fill configuration data.""" + + self.hass = hass + + key_types = dev_block.get(TYPE) + + self.key_values = set() + for key_type in key_types: + self.key_values.add(KEY_VALUE[key_type]) + + self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD) + self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY) + self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT) + self.monitor_task = None + self.dev = None + + async def async_keyrepeat(self, path, name, code, delay, repeat): + """Emulate keyboard delay/repeat behaviour by sending key events on a timer.""" + + await asyncio.sleep(delay) + while True: + self.hass.bus.async_fire( + KEYBOARD_REMOTE_COMMAND_RECEIVED, + {KEY_CODE: code, DEVICE_DESCRIPTOR: path, DEVICE_NAME: name}, ) - self.threads.append(thread) + await asyncio.sleep(repeat) - def run(self): - """Run all event listener threads.""" - for thread in self.threads: - thread.start() + async def async_start_monitoring(self, dev): + """Start event monitoring task and issue event.""" + if self.monitor_task is None: + self.dev = dev + self.monitor_task = self.hass.async_create_task( + self.async_monitor_input(dev) + ) + self.hass.bus.async_fire( + KEYBOARD_REMOTE_CONNECTED, + {DEVICE_DESCRIPTOR: dev.path, DEVICE_NAME: dev.name}, + ) + _LOGGER.debug("Keyboard (re-)connected, %s", dev.name) - def stop(self): - """Stop all event listener threads.""" - for thread in self.threads: - thread.stopped.set() + async def async_stop_monitoring(self): + """Stop event monitoring task and issue event.""" + if self.monitor_task is not None: + try: + self.dev.ungrab() + except OSError: + pass + # monitoring of the device form the event loop and closing of the + # device has to occur before cancelling the task to avoid + # triggering unhandled exceptions inside evdev coroutines + asyncio.get_event_loop().remove_reader(self.dev.fileno()) + self.dev.close() + if not self.monitor_task.done(): + self.monitor_task.cancel() + await self.monitor_task + self.monitor_task = None + self.hass.bus.async_fire( + KEYBOARD_REMOTE_DISCONNECTED, + {DEVICE_DESCRIPTOR: self.dev.path, DEVICE_NAME: self.dev.name}, + ) + _LOGGER.debug("Keyboard disconnected, %s", self.dev.name) + self.dev = None + + async def async_monitor_input(self, dev): + """Event monitoring loop. + + Monitor one device for new events using evdev with asyncio, + start and stop key hold emulation tasks as needed. + """ + + repeat_tasks = {} + + try: + _LOGGER.debug("Start device monitoring") + dev.grab() + async for event in dev.async_read_loop(): + if event.type is ecodes.EV_KEY: + if event.value in self.key_values: + _LOGGER.debug(categorize(event)) + self.hass.bus.async_fire( + KEYBOARD_REMOTE_COMMAND_RECEIVED, + { + KEY_CODE: event.code, + DEVICE_DESCRIPTOR: dev.path, + DEVICE_NAME: dev.name, + }, + ) + + if ( + event.value == KEY_VALUE["key_down"] + and self.emulate_key_hold + ): + repeat_tasks[event.code] = self.hass.async_create_task( + self.async_keyrepeat( + dev.path, + dev.name, + event.code, + self.emulate_key_hold_delay, + self.emulate_key_hold_repeat, + ) + ) + elif event.value == KEY_VALUE["key_up"]: + if event.code in repeat_tasks: + repeat_tasks[event.code].cancel() + del repeat_tasks[event.code] + except (OSError, PermissionError, asyncio.CancelledError): + # cancel key repeat tasks + for task in repeat_tasks.values(): + task.cancel() + + if repeat_tasks: + await asyncio.wait(repeat_tasks.values()) diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index 6172de132bb..25b8bfa682a 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -3,8 +3,8 @@ "name": "Keyboard remote", "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "requirements": [ - "evdev==0.6.1" + "evdev==1.1.2", "aionotify==0.2.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@bendavid"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5679d126e2d..a1db7bbc1f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,6 +169,9 @@ aiolifx==0.6.7 # homeassistant.components.lifx aiolifx_effects==0.2.2 +# homeassistant.components.keyboard_remote +aionotify==0.2.0 + # homeassistant.components.notion aionotion==1.1.0 @@ -477,7 +480,7 @@ epsonprinter==0.0.9 eternalegypt==0.0.10 # homeassistant.components.keyboard_remote -# evdev==0.6.1 +# evdev==1.1.2 # homeassistant.components.evohome evohome-async==0.3.4b1 From 0e4331e922ca1747b6234b206ba704a5f543647a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 29 Oct 2019 00:32:16 +0000 Subject: [PATCH 064/306] [ci skip] Translation update --- .../components/abode/.translations/cs.json | 22 +++++++++++ .../alarm_control_panel/.translations/cs.json | 11 ++++++ .../components/axis/.translations/cs.json | 5 +++ .../binary_sensor/.translations/cs.json | 8 ++++ .../cert_expiry/.translations/cs.json | 8 ++++ .../cert_expiry/.translations/pl.json | 2 +- .../coolmaster/.translations/cs.json | 23 +++++++++++ .../coolmaster/.translations/pl.json | 11 ++++-- .../components/cover/.translations/cs.json | 12 ++++++ .../components/cover/.translations/no.json | 4 +- .../components/cover/.translations/pl.json | 4 +- .../components/daikin/.translations/pl.json | 2 +- .../components/deconz/.translations/cs.json | 1 + .../device_tracker/.translations/cs.json | 8 ++++ .../device_tracker/.translations/no.json | 8 ++++ .../device_tracker/.translations/pl.json | 8 ++++ .../dialogflow/.translations/pl.json | 2 +- .../components/geofency/.translations/pl.json | 2 +- .../gpslogger/.translations/pl.json | 2 +- .../huawei_lte/.translations/cs.json | 38 ++++++++++++++++++ .../huawei_lte/.translations/no.json | 39 +++++++++++++++++++ .../huawei_lte/.translations/pl.json | 39 +++++++++++++++++++ .../components/ifttt/.translations/pl.json | 2 +- .../components/locative/.translations/pl.json | 2 +- .../components/lock/.translations/cs.json | 13 +++++++ .../logi_circle/.translations/pl.json | 2 +- .../components/mailgun/.translations/pl.json | 2 +- .../media_player/.translations/cs.json | 11 ++++++ .../media_player/.translations/no.json | 11 ++++++ .../media_player/.translations/pl.json | 11 ++++++ .../components/neato/.translations/cs.json | 7 ++++ .../components/nest/.translations/pl.json | 4 +- .../opentherm_gw/.translations/pl.json | 2 +- .../owntracks/.translations/pl.json | 2 +- .../components/plaato/.translations/pl.json | 2 +- .../components/plex/.translations/cs.json | 13 +++++++ .../components/plex/.translations/pl.json | 2 +- .../components/point/.translations/pl.json | 2 +- .../components/ps4/.translations/pl.json | 2 +- .../components/sensor/.translations/cs.json | 26 +++++++++++++ .../components/solarlog/.translations/cs.json | 21 ++++++++++ .../components/soma/.translations/cs.json | 14 +++++++ .../components/soma/.translations/pl.json | 2 +- .../components/somfy/.translations/cs.json | 9 +++++ .../components/somfy/.translations/no.json | 5 +++ .../components/somfy/.translations/pl.json | 7 +++- .../tellduslive/.translations/pl.json | 2 +- .../components/traccar/.translations/pl.json | 2 +- .../components/tradfri/.translations/pl.json | 2 +- .../transmission/.translations/cs.json | 17 ++++++++ .../transmission/.translations/pl.json | 5 ++- .../components/twilio/.translations/pl.json | 2 +- .../components/unifi/.translations/cs.json | 9 +++++ .../components/withings/.translations/cs.json | 13 +++++++ .../components/withings/.translations/no.json | 7 ++++ .../components/withings/.translations/pl.json | 7 ++++ 56 files changed, 470 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/abode/.translations/cs.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/cs.json create mode 100644 homeassistant/components/axis/.translations/cs.json create mode 100644 homeassistant/components/binary_sensor/.translations/cs.json create mode 100644 homeassistant/components/cert_expiry/.translations/cs.json create mode 100644 homeassistant/components/coolmaster/.translations/cs.json create mode 100644 homeassistant/components/cover/.translations/cs.json create mode 100644 homeassistant/components/device_tracker/.translations/cs.json create mode 100644 homeassistant/components/device_tracker/.translations/no.json create mode 100644 homeassistant/components/device_tracker/.translations/pl.json create mode 100644 homeassistant/components/huawei_lte/.translations/cs.json create mode 100644 homeassistant/components/huawei_lte/.translations/no.json create mode 100644 homeassistant/components/huawei_lte/.translations/pl.json create mode 100644 homeassistant/components/lock/.translations/cs.json create mode 100644 homeassistant/components/media_player/.translations/cs.json create mode 100644 homeassistant/components/media_player/.translations/no.json create mode 100644 homeassistant/components/media_player/.translations/pl.json create mode 100644 homeassistant/components/neato/.translations/cs.json create mode 100644 homeassistant/components/plex/.translations/cs.json create mode 100644 homeassistant/components/sensor/.translations/cs.json create mode 100644 homeassistant/components/solarlog/.translations/cs.json create mode 100644 homeassistant/components/soma/.translations/cs.json create mode 100644 homeassistant/components/somfy/.translations/cs.json create mode 100644 homeassistant/components/transmission/.translations/cs.json create mode 100644 homeassistant/components/withings/.translations/cs.json diff --git a/homeassistant/components/abode/.translations/cs.json b/homeassistant/components/abode/.translations/cs.json new file mode 100644 index 00000000000..75c65f01e11 --- /dev/null +++ b/homeassistant/components/abode/.translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Je povolena pouze jedna konfigurace Abode." + }, + "error": { + "connection_error": "Nelze se p\u0159ipojit k Abode.", + "identifier_exists": "\u00da\u010det je ji\u017e zaregistrov\u00e1n.", + "invalid_credentials": "Neplatn\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje." + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mailov\u00e1 adresa" + }, + "title": "Vypl\u0148te p\u0159ihla\u0161ovac\u00ed \u00fadaje Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/cs.json b/homeassistant/components/alarm_control_panel/.translations/cs.json new file mode 100644 index 00000000000..247a4e96da4 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/cs.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktivovat {entity_name} v re\u017eimu mimo domov", + "arm_home": "Aktivovat {entity_name} v re\u017eimu doma", + "arm_night": "Aktivovat {entity_name} v re\u017eimu noc", + "disarm": "Deaktivovat {entity_name}", + "trigger": "Spustit {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/cs.json b/homeassistant/components/axis/.translations/cs.json new file mode 100644 index 00000000000..258f301e432 --- /dev/null +++ b/homeassistant/components/axis/.translations/cs.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/cs.json b/homeassistant/components/binary_sensor/.translations/cs.json new file mode 100644 index 00000000000..cb941e67883 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "moist": "{entity_name} se navlh\u010dil", + "not_opened": "{entity_name} uzav\u0159eno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/cs.json b/homeassistant/components/cert_expiry/.translations/cs.json new file mode 100644 index 00000000000..58a5a281ea2 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "certificate_error": "Certifik\u00e1t nelze ov\u011b\u0159it", + "wrong_host": "Certifik\u00e1t neodpov\u00edd\u00e1 n\u00e1zvu hostitele" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json index e594ed66a3f..671cbfcd1ff 100644 --- a/homeassistant/components/cert_expiry/.translations/pl.json +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -6,7 +6,7 @@ "error": { "certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu", "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", - "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z tym hostem", + "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z hostem.", "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107", "wrong_host": "Certyfikat nie pasuje do nazwy hosta" diff --git a/homeassistant/components/coolmaster/.translations/cs.json b/homeassistant/components/coolmaster/.translations/cs.json new file mode 100644 index 00000000000..f1e18f8fcb4 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit k instanci CoolMasterNet. Zkontrolujte pros\u00edm sv\u00e9ho hostitele.", + "no_units": "V hostiteli CoolMasterNet nelze naj\u00edt \u017e\u00e1dn\u00e9 jednotky HVAC." + }, + "step": { + "user": { + "data": { + "cool": "Podpora re\u017eimu chlazen\u00ed", + "dry": "Podpora re\u017eimu vysou\u0161en\u00ed", + "fan_only": "Podpora re\u017eimu pouze ventil\u00e1tor", + "heat": "Podpora re\u017eimu topen\u00ed", + "heat_cool": "Podpora automatick\u00e9ho oh\u0159\u00edv\u00e1n\u00ed/chlazen\u00ed", + "host": "Hostitel", + "off": "Lze vypnout" + }, + "title": "Nastavte podrobnosti p\u0159ipojen\u00ed CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/pl.json b/homeassistant/components/coolmaster/.translations/pl.json index 8568eac2e55..118c4bc424b 100644 --- a/homeassistant/components/coolmaster/.translations/pl.json +++ b/homeassistant/components/coolmaster/.translations/pl.json @@ -10,9 +10,14 @@ "cool": "Obs\u0142uga trybu ch\u0142odzenia", "dry": "Obs\u0142uga trybu osuszania", "fan_only": "Obs\u0142uga trybu \"tylko wentylator\"", - "heat": "Obs\u0142uga tryb grzania" - } + "heat": "Obs\u0142uga trybu grzania", + "heat_cool": "Obs\u0142uga automatycznego trybu grzanie/ch\u0142odzenie", + "host": "Host", + "off": "Mo\u017ce by\u0107 wy\u0142\u0105czone" + }, + "title": "Skonfiguruj szczeg\u00f3\u0142y po\u0142\u0105czenia CoolMasterNet." } - } + }, + "title": "CoolMasterNet" } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/cs.json b/homeassistant/components/cover/.translations/cs.json new file mode 100644 index 00000000000..bed9bc976d3 --- /dev/null +++ b/homeassistant/components/cover/.translations/cs.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} je zav\u0159eno", + "is_closing": "{entity_name} se zav\u00edr\u00e1", + "is_open": "{entity_name} je otev\u0159eno", + "is_opening": "{entity_name} se otev\u00edr\u00e1", + "is_position": "pozice {entity_name} je", + "is_tilt_position": "pozice naklon\u011bn\u00ed {entity_name} je" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json index af567bcfcfc..901ececec1f 100644 --- a/homeassistant/components/cover/.translations/no.json +++ b/homeassistant/components/cover/.translations/no.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} er stengt", "is_closing": "{entity_name} stenges", "is_open": "{entity_name} er \u00e5pen", - "is_opening": "{entity_name} \u00e5pnes" + "is_opening": "{entity_name} \u00e5pnes", + "is_position": "{entity_name}-posisjonen er", + "is_tilt_position": "{entity_name} vippeposisjon er" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json index 4adc0c17b54..a4e4f19712b 100644 --- a/homeassistant/components/cover/.translations/pl.json +++ b/homeassistant/components/cover/.translations/pl.json @@ -4,7 +4,9 @@ "is_closed": "pokrywa {entity_name} jest zamkni\u0119ta", "is_closing": "{entity_name} si\u0119 zamyka", "is_open": "pokrywa {entity_name} jest otwarta", - "is_opening": "{entity_name} si\u0119 otwiera" + "is_opening": "{entity_name} si\u0119 otwiera", + "is_position": "pozycja pokrywy {entity_name} to", + "is_tilt_position": "pochylenie pokrywy {entity_name} to" } } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/pl.json b/homeassistant/components/daikin/.translations/pl.json index 49c5a497667..5d5448a93db 100644 --- a/homeassistant/components/daikin/.translations/pl.json +++ b/homeassistant/components/daikin/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.", - "device_timeout": "Limit czasu pod\u0142\u0105czenia do urz\u0105dzenia." + "device_timeout": "Przekroczono limit czasu \u0142\u0105czenia z urz\u0105dzeniem." }, "step": { "user": { diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 42b14833aa0..c665690796d 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -8,6 +8,7 @@ "error": { "no_key": "Nelze z\u00edskat kl\u00ed\u010d API" }, + "flow_title": "Br\u00e1na deCONZ ZigBee ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/device_tracker/.translations/cs.json b/homeassistant/components/device_tracker/.translations/cs.json new file mode 100644 index 00000000000..778ea0208c4 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} je doma", + "is_not_home": "{entity_name} nen\u00ed doma" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/no.json b/homeassistant/components/device_tracker/.translations/no.json new file mode 100644 index 00000000000..7034378b066 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/no.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} er hjemme", + "is_not_home": "{entity_name} er ikke hjemme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/pl.json b/homeassistant/components/device_tracker/.translations/pl.json new file mode 100644 index 00000000000..8f0f7953a2d --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/pl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "urz\u0105dzenie {entity_name} jest w domu", + "is_not_home": "urz\u0105dzenie {entity_name} jest poza domem" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/pl.json b/homeassistant/components/dialogflow/.translations/pl.json index ee222c83b51..c555a3e09b3 100644 --- a/homeassistant/components/dialogflow/.translations/pl.json +++ b/homeassistant/components/dialogflow/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Dialogflow?", + "description": "Na pewno chcesz skonfigurowa\u0107 Dialogflow?", "title": "Konfiguracja Dialogflow Webhook" } }, diff --git a/homeassistant/components/geofency/.translations/pl.json b/homeassistant/components/geofency/.translations/pl.json index b2b8b606723..e72e99242c1 100644 --- a/homeassistant/components/geofency/.translations/pl.json +++ b/homeassistant/components/geofency/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Geofency?", + "description": "Na pewno chcesz skonfigurowa\u0107 Geofency?", "title": "Konfiguracja Geofency Webhook" } }, diff --git a/homeassistant/components/gpslogger/.translations/pl.json b/homeassistant/components/gpslogger/.translations/pl.json index 726ec2ad9b2..434fdd2220a 100644 --- a/homeassistant/components/gpslogger/.translations/pl.json +++ b/homeassistant/components/gpslogger/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Geofency?", + "description": "Na pewno chcesz skonfigurowa\u0107 Geofency?", "title": "Konfiguracja Geofency Webhook" } }, diff --git a/homeassistant/components/huawei_lte/.translations/cs.json b/homeassistant/components/huawei_lte/.translations/cs.json new file mode 100644 index 00000000000..8d7ac01c55a --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/cs.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Toto za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "connection_failed": "P\u0159ipojen\u00ed se nezda\u0159ilo", + "incorrect_password": "Nespr\u00e1vn\u00e9 heslo", + "incorrect_username": "Nespr\u00e1vn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no", + "incorrect_username_or_password": "Nespr\u00e1vn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no \u010di heslo", + "invalid_url": "Neplatn\u00e1 adresa URL", + "login_attempts_exceeded": "Maxim\u00e1ln\u00ed pokus o p\u0159ihl\u00e1\u0161en\u00ed byl p\u0159ekro\u010den, zkuste to znovu pozd\u011bji", + "response_error": "Nezn\u00e1m\u00e1 chyba ze za\u0159\u00edzen\u00ed", + "unknown_connection_error": "Nezn\u00e1m\u00e1 chyba p\u0159i p\u0159ipojov\u00e1n\u00ed k za\u0159\u00edzen\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "Konfigurovat Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "P\u0159\u00edjemci ozn\u00e1men\u00ed SMS", + "track_new_devices": "Sledovat nov\u00e1 za\u0159\u00edzen\u00ed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/no.json b/homeassistant/components/huawei_lte/.translations/no.json new file mode 100644 index 00000000000..d06a356e998 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/no.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Denne enheten er allerede konfigurert" + }, + "error": { + "connection_failed": "Tilkoblingen mislyktes", + "incorrect_password": "feil passord", + "incorrect_username": "Feil brukernavn", + "incorrect_username_or_password": "Feil brukernavn eller passord", + "invalid_url": "Ugyldig URL-adresse", + "login_attempts_exceeded": "Maksimalt antall p\u00e5loggingsfors\u00f8k er overskredet, vennligst pr\u00f8v igjen senere", + "response_error": "Ukjent feil fra enheten", + "unknown_connection_error": "Ukjent feil under tilkobling til enhet" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "url": "URL", + "username": "Brukernavn" + }, + "description": "Angi detaljer for enhetstilgang. Angivelse av brukernavn og passord er valgfritt, men gir st\u00f8tte for flere integreringsfunksjoner. P\u00e5 den annen side kan bruk av en autorisert tilkobling f\u00f8re til problemer med tilgang til enhetens webgrensesnitt utenfor Home Assistant mens integreringen er aktiv, og omvendt.", + "title": "Konfigurer Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Mottakere av SMS-varsling", + "track_new_devices": "Spor nye enheter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json new file mode 100644 index 00000000000..5a8c4033436 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "connection_failed": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", + "incorrect_password": "Nieprawid\u0142owe has\u0142o", + "incorrect_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", + "incorrect_username_or_password": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", + "invalid_url": "Nieprawid\u0142owy URL", + "login_attempts_exceeded": "Przekroczono maksymaln\u0105 liczb\u0119 pr\u00f3b logowania. Spr\u00f3buj ponownie p\u00f3\u017aniej.", + "response_error": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w urz\u0105dzeniu.", + "unknown_connection_error": "Nieznany b\u0142\u0105d podczas \u0142\u0105czenia z urz\u0105dzeniem" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "url": "URL", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia. Okre\u015blenie nazwy u\u017cytkownika i has\u0142a jest opcjonalne, ale umo\u017cliwia obs\u0142ug\u0119 wi\u0119kszej liczby funkcji integracji. Z drugiej strony u\u017cycie autoryzowanego po\u0142\u0105czenia mo\u017ce powodowa\u0107 problemy z dost\u0119pem do interfejsu internetowego urz\u0105dzenia z zewn\u0105trz Home Assistant'a gdy integracja jest aktywna.", + "title": "Konfiguracja Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Odbiorcy powiadomie\u0144 SMS", + "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json index 270e74945a3..ca81a510531 100644 --- a/homeassistant/components/ifttt/.translations/pl.json +++ b/homeassistant/components/ifttt/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 IFTTT?", + "description": "Na pewno chcesz skonfigurowa\u0107 IFTTT?", "title": "Konfiguracja apletu Webhook IFTTT" } }, diff --git a/homeassistant/components/locative/.translations/pl.json b/homeassistant/components/locative/.translations/pl.json index 917744c32fd..9c22a8e3fea 100644 --- a/homeassistant/components/locative/.translations/pl.json +++ b/homeassistant/components/locative/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy na pewno chcesz skonfigurowa\u0107 Locative Webhook?", + "description": "Na pewno chcesz skonfigurowa\u0107 Locative Webhook?", "title": "Skonfiguruj Locative Webhook" } }, diff --git a/homeassistant/components/lock/.translations/cs.json b/homeassistant/components/lock/.translations/cs.json new file mode 100644 index 00000000000..3843248a9ee --- /dev/null +++ b/homeassistant/components/lock/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "Zamknout {entity_name}", + "open": "Otev\u0159\u00edt {entity_name}", + "unlock": "Odemknout {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} je uzam\u010deno", + "is_unlocked": "{entity_name} je odem\u010deno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json index 5d8e6a0607d..2266ea841c5 100644 --- a/homeassistant/components/logi_circle/.translations/pl.json +++ b/homeassistant/components/logi_circle/.translations/pl.json @@ -11,7 +11,7 @@ }, "error": { "auth_error": "Autoryzacja API nie powiod\u0142a si\u0119.", - "auth_timeout": "Up\u0142yn\u0105\u0142 limit czasu \u017c\u0105dania tokena dost\u0119pu.", + "auth_timeout": "Przekroczono limit czasu \u017c\u0105dania tokena dost\u0119pu.", "follow_link": "Post\u0119puj zgodnie z linkiem i uwierzytelnij si\u0119 przed naci\u015bni\u0119ciem przycisku Prze\u015blij." }, "step": { diff --git a/homeassistant/components/mailgun/.translations/pl.json b/homeassistant/components/mailgun/.translations/pl.json index ccdc368afff..ddca4c432fa 100644 --- a/homeassistant/components/mailgun/.translations/pl.json +++ b/homeassistant/components/mailgun/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Mailgun?", + "description": "Na pewno chcesz skonfigurowa\u0107 Mailgun?", "title": "Konfiguracja Mailgun Webhook" } }, diff --git a/homeassistant/components/media_player/.translations/cs.json b/homeassistant/components/media_player/.translations/cs.json new file mode 100644 index 00000000000..afda756740a --- /dev/null +++ b/homeassistant/components/media_player/.translations/cs.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} je ne\u010dinn\u00fd", + "is_off": "{entity_name} je vypnuto", + "is_on": "{entity_name} je zapnuto", + "is_paused": "{entity_name} je pozastaven", + "is_playing": "{entity_name} p\u0159ehr\u00e1v\u00e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/no.json b/homeassistant/components/media_player/.translations/no.json new file mode 100644 index 00000000000..a05d907774f --- /dev/null +++ b/homeassistant/components/media_player/.translations/no.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} er inaktiv", + "is_off": "{entity_name} er sl\u00e5tt av", + "is_on": "{entity_name} er sl\u00e5tt p\u00e5", + "is_paused": "{entity_name} er satt p\u00e5 pause", + "is_playing": "{entity_name} spiller" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/pl.json b/homeassistant/components/media_player/.translations/pl.json new file mode 100644 index 00000000000..29b3beaee63 --- /dev/null +++ b/homeassistant/components/media_player/.translations/pl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "odtwarzacz medi\u00f3w (entity_name} jest nieaktywny", + "is_off": "odtwarzacz medi\u00f3w (entity_name} jest wy\u0142\u0105czony", + "is_on": "odtwarzacz medi\u00f3w (entity_name} jest w\u0142\u0105czony", + "is_paused": "odtwarzanie medi\u00f3w na {entity_name} jest wstrzymane", + "is_playing": "{entity_name} odtwarza media" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/cs.json b/homeassistant/components/neato/.translations/cs.json new file mode 100644 index 00000000000..2a448a26e40 --- /dev/null +++ b/homeassistant/components/neato/.translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unexpected_error": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json index ec33346cdf8..482a67eb221 100644 --- a/homeassistant/components/nest/.translations/pl.json +++ b/homeassistant/components/nest/.translations/pl.json @@ -3,13 +3,13 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", "invalid_code": "Nieprawid\u0142owy kod", - "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu", + "timeout": "Przekroczono limit czasu sprawdzania poprawno\u015bci kodu.", "unknown": "Nieznany b\u0142\u0105d sprawdzania poprawno\u015bci kodu" }, "step": { diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index e4403420b11..180f6a2430d 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Bramka jest ju\u017c skonfigurowana", "id_exists": "Identyfikator bramki ju\u017c istnieje", "serial_error": "B\u0142\u0105d po\u0142\u0105czenia z urz\u0105dzeniem", - "timeout": "Up\u0142yn\u0105\u0142 limit czasu pr\u00f3by po\u0142\u0105czenia" + "timeout": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia." }, "step": { "init": { diff --git a/homeassistant/components/owntracks/.translations/pl.json b/homeassistant/components/owntracks/.translations/pl.json index fd6cba18237..91afa020fc0 100644 --- a/homeassistant/components/owntracks/.translations/pl.json +++ b/homeassistant/components/owntracks/.translations/pl.json @@ -8,7 +8,7 @@ }, "step": { "user": { - "description": "Czy na pewno chcesz skonfigurowa\u0107 OwnTracks?", + "description": "Na pewno chcesz skonfigurowa\u0107 OwnTracks?", "title": "Konfiguracja OwnTracks" } }, diff --git a/homeassistant/components/plaato/.translations/pl.json b/homeassistant/components/plaato/.translations/pl.json index aac48ee4774..c4402cb8f37 100644 --- a/homeassistant/components/plaato/.translations/pl.json +++ b/homeassistant/components/plaato/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy na pewno chcesz skonfigurowa\u0107 Airlock Plaato?", + "description": "Na pewno chcesz skonfigurowa\u0107 Airlock Plaato?", "title": "Konfiguracja Plaato Webhook" } }, diff --git a/homeassistant/components/plex/.translations/cs.json b/homeassistant/components/plex/.translations/cs.json new file mode 100644 index 00000000000..e033cd5c514 --- /dev/null +++ b/homeassistant/components/plex/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "discovery_no_file": "Nebyl nalezen \u017e\u00e1dn\u00fd star\u0161\u00ed konfigura\u010dn\u00ed soubor" + }, + "step": { + "start_website_auth": { + "description": "Pokra\u010dujte v autorizaci na plex.tv.", + "title": "P\u0159ipojit server plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index 0b94e3eacb6..b4ed6134106 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -6,7 +6,7 @@ "already_in_progress": "Plex jest konfigurowany", "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", - "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", + "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena.", "unknown": "Nieznany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/point/.translations/pl.json b/homeassistant/components/point/.translations/pl.json index ca36001cc1a..40acc8b4e49 100644 --- a/homeassistant/components/point/.translations/pl.json +++ b/homeassistant/components/point/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko konto Point.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "external_setup": "Punkt pomy\u015blnie skonfigurowany.", "no_flows": "Musisz skonfigurowa\u0107 Point, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/point/)." }, diff --git a/homeassistant/components/ps4/.translations/pl.json b/homeassistant/components/ps4/.translations/pl.json index 9fb4c73f1d0..0770116f1c8 100644 --- a/homeassistant/components/ps4/.translations/pl.json +++ b/homeassistant/components/ps4/.translations/pl.json @@ -8,7 +8,7 @@ "port_997_bind_error": "Nie mo\u017cna powi\u0105za\u0107 z portem 997." }, "error": { - "credential_timeout": "Up\u0142yn\u0105\u0142 limit czasu us\u0142ugi po\u015bwiadcze\u0144. Naci\u015bnij przycisk Prze\u015blij, aby ponowi\u0107.", + "credential_timeout": "Przekroczono limit czasu us\u0142ugi po\u015bwiadcze\u0144. Naci\u015bnij przycisk Prze\u015blij, aby ponowi\u0107.", "login_failed": "Nie uda\u0142o si\u0119 sparowa\u0107 z PlayStation 4. Sprawd\u017a, czy PIN jest poprawny.", "no_ipaddress": "Wprowad\u017a adres IP PlayStation 4, kt\u00f3ry chcesz skonfigurowa\u0107.", "not_ready": "PlayStation 4 nie jest w\u0142\u0105czona lub po\u0142\u0105czona z sieci\u0105." diff --git a/homeassistant/components/sensor/.translations/cs.json b/homeassistant/components/sensor/.translations/cs.json new file mode 100644 index 00000000000..1b2dbad1a4c --- /dev/null +++ b/homeassistant/components/sensor/.translations/cs.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Aktu\u00e1ln\u00ed \u00farove\u0148 nabit\u00ed baterie {entity_name}", + "is_humidity": "Aktu\u00e1ln\u00ed vlhkost {entity_name}", + "is_illuminance": "Aktu\u00e1ln\u00ed osv\u011btlen\u00ed {entity_name}", + "is_power": "Aktu\u00e1ln\u00ed v\u00fdkon {entity_name}", + "is_pressure": "Aktu\u00e1ln\u00ed tlak {entity_name}", + "is_signal_strength": "Aktu\u00e1ln\u00ed s\u00edla sign\u00e1lu {entity_name}", + "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", + "is_timestamp": "Aktu\u00e1ln\u00ed \u010dasov\u00e9 raz\u00edtko {entity_name}", + "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}" + }, + "trigger_type": { + "battery_level": "\u00farove\u0148 baterie {entity_name} se zm\u011bn\u00ed", + "humidity": "vlhkost {entity_name} se zm\u011bn\u00ed", + "illuminance": "osv\u011btlen\u00ed {entity_name} se zm\u011bn\u00ed", + "power": "el. v\u00fdkon {entity_name} se zm\u011bn\u00ed", + "pressure": "tlak {entity_name} se zm\u011bn\u00ed", + "signal_strength": "s\u00edla sign\u00e1lu {entity_name} se zm\u011bn\u00ed", + "temperature": "teplota {entity_name} se zm\u011bn\u00ed", + "timestamp": "\u010dasov\u00e9 raz\u00edtko {entity_name} se zm\u011bn\u00ed", + "value": "hodnota {entity_name} se zm\u011bn\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/cs.json b/homeassistant/components/solarlog/.translations/cs.json new file mode 100644 index 00000000000..f2294823ebb --- /dev/null +++ b/homeassistant/components/solarlog/.translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit, ov\u011b\u0159te pros\u00edm adresu hostitele" + }, + "step": { + "user": { + "data": { + "host": "N\u00e1zev hostitele nebo IP adresa va\u0161eho za\u0159\u00edzen\u00ed Solar-Log", + "name": "Prefix, kter\u00fd se m\u00e1 pou\u017e\u00edt pro va\u0161e senzory Solar-Log" + }, + "title": "Definujte sv\u00e9 p\u0159ipojen\u00ed Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/cs.json b/homeassistant/components/soma/.translations/cs.json new file mode 100644 index 00000000000..b3922b67795 --- /dev/null +++ b/homeassistant/components/soma/.translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + }, + "description": "Zadejte pros\u00edm nastaven\u00ed p\u0159ipojen\u00ed va\u0161eho SOMA Connect.", + "title": "SOMA Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index 4d783f3f0a0..c71e160142e 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { diff --git a/homeassistant/components/somfy/.translations/cs.json b/homeassistant/components/somfy/.translations/cs.json new file mode 100644 index 00000000000..7ba035f562e --- /dev/null +++ b/homeassistant/components/somfy/.translations/cs.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/no.json b/homeassistant/components/somfy/.translations/no.json index 9d82eea3511..f5944cbf9c7 100644 --- a/homeassistant/components/somfy/.translations/no.json +++ b/homeassistant/components/somfy/.translations/no.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Vellykket autentisering med Somfy." }, + "step": { + "pick_implementation": { + "title": "Velg autentiseringsmetode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/pl.json b/homeassistant/components/somfy/.translations/pl.json index cb19fcb793a..e08e921cf1a 100644 --- a/homeassistant/components/somfy/.translations/pl.json +++ b/homeassistant/components/somfy/.translations/pl.json @@ -2,12 +2,17 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Somfy.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "missing_configuration": "Komponent Somfy nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Somfy" }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json index 06391b24b99..01d3c7125c3 100644 --- a/homeassistant/components/tellduslive/.translations/pl.json +++ b/homeassistant/components/tellduslive/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "TelldusLive jest ju\u017c skonfigurowany", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json index 66ddbaaa3fd..74ff0c089d8 100644 --- a/homeassistant/components/traccar/.translations/pl.json +++ b/homeassistant/components/traccar/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy na pewno chcesz skonfigurowa\u0107 Traccar?", + "description": "Na pewno chcesz skonfigurowa\u0107 Traccar?", "title": "Skonfiguruj Traccar" } }, diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index 3a1798e66d9..fc115294031 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z bram\u0105.", "invalid_key": "Rejestracja si\u0119 nie powiod\u0142a z podanym kluczem. Je\u015bli tak si\u0119 stanie, spr\u00f3buj ponownie uruchomi\u0107 bramk\u0119.", - "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu" + "timeout": "Przekroczono limit czasu sprawdzania poprawno\u015bci kodu." }, "step": { "auth": { diff --git a/homeassistant/components/transmission/.translations/cs.json b/homeassistant/components/transmission/.translations/cs.json new file mode 100644 index 00000000000..bb96d5809e1 --- /dev/null +++ b/homeassistant/components/transmission/.translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Hostitel je ji\u017e nakonfigurov\u00e1n." + }, + "error": { + "name_exists": "Jm\u00e9no ji\u017e existuje" + } + }, + "options": { + "step": { + "init": { + "title": "Nakonfigurujte mo\u017enosti pro Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json index 9b45e310767..a85a3f9b006 100644 --- a/homeassistant/components/transmission/.translations/pl.json +++ b/homeassistant/components/transmission/.translations/pl.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Host jest ju\u017c skonfigurowany.", "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", + "name_exists": "Nazwa ju\u017c istnieje", "wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" }, - "description": "Konfiguracja opcji dla Transmission" + "description": "Konfiguracja opcji dla Transmission", + "title": "Konfiguracja opcji dla Transmission" } } } diff --git a/homeassistant/components/twilio/.translations/pl.json b/homeassistant/components/twilio/.translations/pl.json index 2b963ff1be5..c61d22db880 100644 --- a/homeassistant/components/twilio/.translations/pl.json +++ b/homeassistant/components/twilio/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Twilio?", + "description": "Na pewno chcesz skonfigurowa\u0107 Twilio?", "title": "Konfiguracja Twilio Webhook" } }, diff --git a/homeassistant/components/unifi/.translations/cs.json b/homeassistant/components/unifi/.translations/cs.json index 3ea631ec86c..32711da56f7 100644 --- a/homeassistant/components/unifi/.translations/cs.json +++ b/homeassistant/components/unifi/.translations/cs.json @@ -22,5 +22,14 @@ } }, "title": "UniFi \u0159adi\u010d" + }, + "options": { + "step": { + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro s\u00ed\u0165ov\u00e9 klienty" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/cs.json b/homeassistant/components/withings/.translations/cs.json new file mode 100644 index 00000000000..a8aea1fa08f --- /dev/null +++ b/homeassistant/components/withings/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Kter\u00fd profil jste vybrali na webu Withings? Je d\u016fle\u017eit\u00e9, aby se profily shodovaly, jinak budou data nespr\u00e1vn\u011b ozna\u010dena.", + "title": "U\u017eivatelsk\u00fd profil." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json index d32c9640fd7..bdde342e7bc 100644 --- a/homeassistant/components/withings/.translations/no.json +++ b/homeassistant/components/withings/.translations/no.json @@ -7,6 +7,13 @@ "default": "Vellykket autentisering for Withings og den valgte profilen." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Hvilken profil valgte du p\u00e5 Withings nettsted? Det er viktig at profilene samsvarer, ellers blir data feilmerket.", + "title": "Brukerprofil." + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 3c345a1a788..4f1ee47ab0e 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -7,6 +7,13 @@ "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Kt\u00f3ry profil wybra\u0142e\u015b na stronie Withings? Wa\u017cne jest, aby profile si\u0119 zgadza\u0142y, w przeciwnym razie dane zostan\u0105 b\u0142\u0119dnie oznaczone.", + "title": "Profil u\u017cytkownika" + }, "user": { "data": { "profile": "Profil" From a0f764cf6da52bfce9cf80b5f693fb06df62ebab Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 29 Oct 2019 01:44:26 +0100 Subject: [PATCH 065/306] Remove blocking I/O from the event loop (#28305) --- homeassistant/components/xiaomi_miio/fan.py | 2 +- homeassistant/components/xiaomi_miio/light.py | 2 +- homeassistant/components/xiaomi_miio/remote.py | 2 +- homeassistant/components/xiaomi_miio/sensor.py | 2 +- homeassistant/components/xiaomi_miio/switch.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index acac60e108a..e50c0102925 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -445,7 +445,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if model is None: try: miio_device = Device(host, token) - device_info = miio_device.info() + device_info = await hass.async_add_executor_job(miio_device.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 3d23f1dfc98..cb16fa094ce 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -134,7 +134,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if model is None: try: miio_device = Device(host, token) - device_info = miio_device.info() + device_info = await hass.async_add_executor_job(miio_device.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 311a356870c..9898f0f88b4 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -88,7 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Check that we can communicate with device. try: - device_info = device.info() + device_info = await hass.async_add_executor_job(device.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 0ebffb06fcd..253f2c967df 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -50,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: air_quality_monitor = AirQualityMonitor(host, token) - device_info = air_quality_monitor.info() + device_info = await hass.async_add_executor_job(air_quality_monitor.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 7fa1638253c..4b9246a5470 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -120,7 +120,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if model is None: try: miio_device = Device(host, token) - device_info = miio_device.info() + device_info = await hass.async_add_executor_job(miio_device.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( From 5b96704c4aec1d90d879a6fc0d75faaa469f061f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 29 Oct 2019 01:45:22 +0100 Subject: [PATCH 066/306] Use dict[key] for required config keys (#28304) * Use dict[key] for required config keys * Change CONF_NAME too because it has a default --- homeassistant/components/xiaomi_miio/device_tracker.py | 4 ++-- homeassistant/components/xiaomi_miio/fan.py | 6 +++--- homeassistant/components/xiaomi_miio/light.py | 6 +++--- homeassistant/components/xiaomi_miio/remote.py | 4 ++-- homeassistant/components/xiaomi_miio/sensor.py | 6 +++--- homeassistant/components/xiaomi_miio/switch.py | 6 +++--- homeassistant/components/xiaomi_miio/vacuum.py | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index 6c2c391034e..e2611b52f12 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -26,8 +26,8 @@ def get_scanner(hass, config): from miio import WifiRepeater, DeviceException scanner = None - host = config[DOMAIN].get(CONF_HOST) - token = config[DOMAIN].get(CONF_TOKEN) + host = config[DOMAIN][CONF_HOST] + token = config[DOMAIN][CONF_TOKEN] _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index e50c0102925..e6c356b7338 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -434,9 +434,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index cb16fa094ce..aa5a0ed42b9 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -121,9 +121,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 9898f0f88b4..075dd15e887 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -75,8 +75,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" from miio import ChuangmiIr, DeviceException - host = config.get(CONF_HOST) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] # Create handler _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 253f2c967df..c19e314acdd 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -42,9 +42,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 4b9246a5470..97e8ef27c3f 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -107,9 +107,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 61ddef92739..aa08693db63 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -182,9 +182,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] # Create handler _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) From 04ab20846a7a430d6ce38b5fa8c1eeb80addc474 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Oct 2019 07:32:34 +0100 Subject: [PATCH 067/306] Bump black to 19.10b0 (#28310) --- .pre-commit-config.yaml | 2 +- homeassistant/auth/mfa_modules/totp.py | 6 ++++- .../components/androidtv/media_player.py | 10 +++++--- homeassistant/components/envirophat/sensor.py | 25 ++++++++++++------- .../components/hangouts/hangouts_bot.py | 7 +++--- .../components/here_travel_time/sensor.py | 2 +- homeassistant/loader.py | 4 +-- homeassistant/util/color.py | 2 +- homeassistant/util/location.py | 2 +- homeassistant/util/yaml/loader.py | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecobee/test_config_flow.py | 4 +-- tests/components/withings/test_common.py | 4 +-- tests/components/zwave/test_cover.py | 14 +++++------ tests/components/zwave/test_init.py | 4 +-- 16 files changed, 54 insertions(+), 38 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 268cff9ea78..e2b99393639 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 19.3b0 + rev: 19.10b0 hooks: - id: black args: diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 9829044a53e..9b0f3910e92 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -215,7 +215,11 @@ class TotpSetupFlow(SetupFlow): else: hass = self._auth_module.hass - self._ota_secret, self._url, self._image = await hass.async_add_executor_job( + ( + self._ota_secret, + self._url, + self._image, + ) = await hass.async_add_executor_job( _generate_secret_and_qr_code, # type: ignore str(self._user.name), ) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 62ae93f96e4..60f423c3dee 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -455,9 +455,13 @@ class AndroidTVDevice(ADBDevice): return # Get the updated state and attributes. - state, self._current_app, self._device, self._is_volume_muted, self._volume_level = ( - self.aftv.update() - ) + ( + state, + self._current_app, + self._device, + self._is_volume_muted, + self._volume_level, + ) = self.aftv.update() self._state = ANDROIDTV_STATES.get(state) if self._state is None: diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 459d21ab698..2aaeefa48cf 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -172,14 +172,18 @@ class EnvirophatData: self.envirophat.leds.off() # accelerometer readings in G - self.accelerometer_x, self.accelerometer_y, self.accelerometer_z = ( - self.envirophat.motion.accelerometer() - ) + ( + self.accelerometer_x, + self.accelerometer_y, + self.accelerometer_z, + ) = self.envirophat.motion.accelerometer() # raw magnetometer reading - self.magnetometer_x, self.magnetometer_y, self.magnetometer_z = ( - self.envirophat.motion.magnetometer() - ) + ( + self.magnetometer_x, + self.magnetometer_y, + self.magnetometer_z, + ) = self.envirophat.motion.magnetometer() # temperature resolution of BMP280 sensor: 0.01°C self.temperature = round(self.envirophat.weather.temperature(), 2) @@ -189,6 +193,9 @@ class EnvirophatData: self.pressure = round(self.envirophat.weather.pressure() / 100.0, 3) # Voltage sensor, reading between 0-3.3V - self.voltage_0, self.voltage_1, self.voltage_2, self.voltage_3 = ( - self.envirophat.analog.read_all() - ) + ( + self.voltage_0, + self.voltage_1, + self.voltage_2, + self.voltage_3, + ) = self.envirophat.analog.read_all() diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 9fc3e2fa58e..9eee2ca2be3 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -308,9 +308,10 @@ class HangoutsBot: async def _async_list_conversations(self): import hangups - self._user_list, self._conversation_list = await hangups.build_user_conversation_list( - self._client - ) + ( + self._user_list, + self._conversation_list, + ) = await hangups.build_user_conversation_list(self._client) conversations = {} for i, conv in enumerate(self._conversation_list.get_all()): users_in_conversation = [] diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index b752b82d087..afd12bb01c5 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -237,7 +237,7 @@ class HERETravelTimeSensor(Entity): @property def device_state_attributes( - self + self, ) -> Optional[Dict[str, Union[None, float, str, bool]]]: """Return the state attributes.""" if self._here_data.base_time is None: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 272271eefb3..e32303ecfab 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -64,7 +64,7 @@ def manifest_from_legacy_module(domain: str, module: ModuleType) -> Dict: async def _async_get_custom_components( - hass: "HomeAssistant" + hass: "HomeAssistant", ) -> Dict[str, "Integration"]: """Return list of custom integrations.""" try: @@ -102,7 +102,7 @@ async def _async_get_custom_components( async def async_get_custom_components( - hass: "HomeAssistant" + hass: "HomeAssistant", ) -> Dict[str, "Integration"]: """Return cached list of custom integrations.""" reg_or_evt = hass.data.get(DATA_CUSTOM_COMPONENTS) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 640e5c5540a..837a0e77cd7 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -441,7 +441,7 @@ def color_temperature_to_hs(color_temperature_kelvin: float) -> Tuple[float, flo def color_temperature_to_rgb( - color_temperature_kelvin: float + color_temperature_kelvin: float, ) -> Tuple[float, float, float]: """ Return an RGB color from a color temperature in Kelvin. diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 7c61a8ab1e9..b572b3025a0 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -46,7 +46,7 @@ LocationInfo = collections.namedtuple( async def async_detect_location_info( - session: aiohttp.ClientSession + session: aiohttp.ClientSession, ) -> Optional[LocationInfo]: """Detect location information.""" data = await _get_ipapi(session) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 9822b7c63c2..bbbe22c2ba8 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -225,7 +225,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: """Add line number and file name to Load YAML sequence.""" - obj, = loader.construct_yaml_seq(node) + (obj,) = loader.construct_yaml_seq(node) return _add_reference(obj, loader, node) diff --git a/requirements_test.txt b/requirements_test.txt index 5240946b004..ad46cfa0741 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ # When updating this file, update .pre-commit-config.yaml too asynctest==0.13.0 -black==19.3b0 +black==19.10b0 codecov==2.0.15 flake8-docstrings==1.5.0 flake8==3.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a13a44288a..a4aaeb373cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -5,7 +5,7 @@ # When updating this file, update .pre-commit-config.yaml too asynctest==0.13.0 -black==19.3b0 +black==19.10b0 codecov==2.0.15 flake8-docstrings==1.5.0 flake8==3.7.8 diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 4008e6a17b1..64f0e3df0e7 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -131,7 +131,7 @@ async def test_import_flow_triggered_but_no_ecobee_conf(hass): async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_tokens( - hass + hass, ): """Test expected result if import flow triggers and ecobee.conf exists with valid tokens.""" flow = config_flow.EcobeeFlowHandler() @@ -181,7 +181,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_invalid_data(hass): async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_tokens( - hass + hass, ): """Test expected result if import flow triggers and ecobee.conf exists with stale tokens.""" flow = config_flow.EcobeeFlowHandler() diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index e513ebb1d2e..f6075c0f734 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -75,7 +75,7 @@ async def test_data_manager_call(data_manager: WithingsDataManager) -> None: async def test_data_manager_call_throttle_enabled( - data_manager: WithingsDataManager + data_manager: WithingsDataManager, ) -> None: """Test method.""" hello_func = MagicMock(return_value="HELLO2") @@ -90,7 +90,7 @@ async def test_data_manager_call_throttle_enabled( async def test_data_manager_call_throttle_disabled( - data_manager: WithingsDataManager + data_manager: WithingsDataManager, ) -> None: """Test method.""" hello_func = MagicMock(return_value="HELLO2") diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index a8580604e46..848decc2fb5 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -130,17 +130,17 @@ def test_roller_commands(hass, mock_openzwave): device.open_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == open_value.value_id device.close_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == close_value.value_id device.stop_cover() assert mock_network.manager.releaseButton.called - value_id, = mock_network.manager.releaseButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.releaseButton.mock_calls.pop(0)[1] assert value_id == open_value.value_id @@ -168,7 +168,7 @@ def test_roller_invert_percent(hass, mock_openzwave): device.open_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == open_value.value_id @@ -193,17 +193,17 @@ def test_roller_reverse_open_close(hass, mock_openzwave): device.open_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == close_value.value_id device.close_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == open_value.value_id device.stop_cover() assert mock_network.manager.releaseButton.called - value_id, = mock_network.manager.releaseButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.releaseButton.mock_calls.pop(0)[1] assert value_id == close_value.value_id diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index f32c97506f8..1de69249bfe 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1610,10 +1610,10 @@ class TestZWaveServices(unittest.TestCase): self.hass.block_till_done() assert self.zwave_network.manager.pressButton.called - value_id, = self.zwave_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = self.zwave_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == reset_value.value_id assert self.zwave_network.manager.releaseButton.called - value_id, = self.zwave_network.manager.releaseButton.mock_calls.pop(0)[1] + (value_id,) = self.zwave_network.manager.releaseButton.mock_calls.pop(0)[1] assert value_id == reset_value.value_id def test_add_association(self): From a4ec4d5a182f74ab741872c6979ad8e101d46fda Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Oct 2019 07:32:57 +0100 Subject: [PATCH 068/306] Add source constants for all config entry discovery sources (#28311) --- homeassistant/config_entries.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ae7c534adf8..ae3aeebb1ee 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -20,9 +20,11 @@ from homeassistant.helpers.event import Event _LOGGER = logging.getLogger(__name__) _UNDEF: dict = {} -SOURCE_USER = "user" SOURCE_DISCOVERY = "discovery" SOURCE_IMPORT = "import" +SOURCE_SSDP = "ssdp" +SOURCE_USER = "user" +SOURCE_ZEROCONF = "zeroconf" HANDLERS = Registry() @@ -50,7 +52,7 @@ ENTRY_STATE_FAILED_UNLOAD = "failed_unload" UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD) DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" -DISCOVERY_SOURCES = ("ssdp", "zeroconf", SOURCE_DISCOVERY, SOURCE_IMPORT) +DISCOVERY_SOURCES = (SOURCE_SSDP, SOURCE_ZEROCONF, SOURCE_DISCOVERY, SOURCE_IMPORT) EVENT_FLOW_DISCOVERED = "config_entry_discovered" From 79ac77a93d37bcfae4161c43e6666f6fce68936a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Oct 2019 23:47:31 -0700 Subject: [PATCH 069/306] Almond integration (#28282) * Initial Almond integration * Hassfest * Update library * Address comments * Fix inheritance issue py36 * Remove no longer needed check * Fix time --- CODEOWNERS | 1 + homeassistant/auth/__init__.py | 8 +- .../components/almond/.translations/en.json | 8 + homeassistant/components/almond/__init__.py | 230 ++++++++++++++++++ .../components/almond/config_flow.py | 125 ++++++++++ homeassistant/components/almond/const.py | 4 + homeassistant/components/almond/manifest.json | 9 + homeassistant/components/almond/strings.json | 9 + homeassistant/generated/config_flows.py | 1 + .../helpers/config_entry_oauth2_flow.py | 13 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/almond/__init__.py | 1 + tests/components/almond/test_config_flow.py | 138 +++++++++++ 14 files changed, 544 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/almond/.translations/en.json create mode 100644 homeassistant/components/almond/__init__.py create mode 100644 homeassistant/components/almond/config_flow.py create mode 100644 homeassistant/components/almond/const.py create mode 100644 homeassistant/components/almond/manifest.json create mode 100644 homeassistant/components/almond/strings.json create mode 100644 tests/components/almond/__init__.py create mode 100644 tests/components/almond/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index dac59039935..aed575b5271 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,6 +19,7 @@ homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy +homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 921bec71e78..3f7dd570400 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -261,7 +261,7 @@ class AuthManager: """Enable a multi-factor auth module for user.""" if user.system_generated: raise ValueError( - "System generated users cannot enable " "multi-factor auth module." + "System generated users cannot enable multi-factor auth module." ) module = self.get_auth_mfa_module(mfa_module_id) @@ -276,7 +276,7 @@ class AuthManager: """Disable a multi-factor auth module for user.""" if user.system_generated: raise ValueError( - "System generated users cannot disable " "multi-factor auth module." + "System generated users cannot disable multi-factor auth module." ) module = self.get_auth_mfa_module(mfa_module_id) @@ -320,7 +320,7 @@ class AuthManager: if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM): raise ValueError( - "System generated users can only have system type " "refresh tokens" + "System generated users can only have system type refresh tokens" ) if token_type == models.TOKEN_TYPE_NORMAL and client_id is None: @@ -330,7 +330,7 @@ class AuthManager: token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and client_name is None ): - raise ValueError("Client_name is required for long-lived access " "token") + raise ValueError("Client_name is required for long-lived access token") if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN: for token in user.refresh_tokens.values(): diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json new file mode 100644 index 00000000000..cc48b1c28eb --- /dev/null +++ b/homeassistant/components/almond/.translations/en.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Almond account." + }, + "title": "Almond" + } +} diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py new file mode 100644 index 00000000000..ebdddecdec3 --- /dev/null +++ b/homeassistant/components/almond/__init__.py @@ -0,0 +1,230 @@ +"""Support for Almond.""" +import asyncio +from datetime import timedelta +import logging +import time + +import async_timeout +from aiohttp import ClientSession, ClientError +from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI +import voluptuous as vol + +from homeassistant.const import CONF_TYPE, CONF_HOST +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.helpers import ( + config_validation as cv, + config_entry_oauth2_flow, + intent, + aiohttp_client, + storage, +) +from homeassistant import config_entries +from homeassistant.components import conversation + +from . import config_flow +from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 + +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" + +STORAGE_VERSION = 1 +STORAGE_KEY = DOMAIN + +DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu" +DEFAULT_LOCAL_HOST = "http://localhost:3000" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Any( + vol.Schema( + { + vol.Required(CONF_TYPE): TYPE_OAUTH2, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_OAUTH2_HOST): cv.url, + } + ), + vol.Schema( + {vol.Required(CONF_TYPE): TYPE_LOCAL, vol.Required(CONF_HOST): cv.url} + ), + ) + }, + extra=vol.ALLOW_EXTRA, +) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the Almond component.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + host = conf[CONF_HOST] + + if conf[CONF_TYPE] == TYPE_OAUTH2: + config_flow.AlmondFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], + f"{host}/me/api/oauth2/authorize", + f"{host}/me/api/oauth2/token", + ), + ) + return True + + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]}, + ) + ) + return True + + +async def async_setup_entry(hass, entry): + """Set up Almond config entry.""" + websession = aiohttp_client.async_get_clientsession(hass) + if entry.data["type"] == TYPE_LOCAL: + auth = AlmondLocalAuth(entry.data["host"], websession) + + else: + # OAuth2 + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + oauth_session = config_entry_oauth2_flow.OAuth2Session( + hass, entry, implementation + ) + auth = AlmondOAuth(entry.data["host"], websession, oauth_session) + + api = WebAlmondAPI(auth) + agent = AlmondAgent(api) + + # Hass.io does its own configuration of Almond. + if entry.data.get("is_hassio"): + conversation.async_set_agent(hass, agent) + return True + + # Configure Almond to connect to Home Assistant + store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + data = await store.async_load() + + if data is None: + data = {} + + user = None + if "almond_user" in data: + user = await hass.auth.async_get_user(data["almond_user"]) + + if user is None: + user = await hass.auth.async_create_system_user("Almond", [GROUP_ID_ADMIN]) + data["almond_user"] = user.id + await store.async_save(data) + + refresh_token = await hass.auth.async_create_refresh_token( + user, + # Almond will be fine as long as we restart once every 5 years + access_token_expiration=timedelta(days=365 * 5), + ) + + # Create long lived access token + access_token = hass.auth.async_create_access_token(refresh_token) + + # Store token in Almond + try: + with async_timeout.timeout(10): + await api.async_create_device( + { + "kind": "io.home-assistant", + "hassUrl": hass.config.api.base_url, + "accessToken": access_token, + "refreshToken": "", + # 5 years from now in ms. + "accessTokenExpires": (time.time() + 60 * 60 * 24 * 365 * 5) * 1000, + } + ) + except (asyncio.TimeoutError, ClientError) as err: + if isinstance(err, asyncio.TimeoutError): + msg = "Request timeout" + else: + msg = err + _LOGGER.warning("Unable to configure Almond: %s", msg) + await hass.auth.async_remove_refresh_token(refresh_token) + raise ConfigEntryNotReady + + # Clear all other refresh tokens + for token in list(user.refresh_tokens.values()): + if token.id != refresh_token.id: + await hass.auth.async_remove_refresh_token(token) + + conversation.async_set_agent(hass, agent) + return True + + +async def async_unload_entry(hass, entry): + """Unload Almond.""" + conversation.async_set_agent(hass, None) + return True + + +class AlmondOAuth(AbstractAlmondWebAuth): + """Almond Authentication using OAuth2.""" + + def __init__( + self, + host: str, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ): + """Initialize Almond auth.""" + super().__init__(host, websession) + self._oauth_session = oauth_session + + async def async_get_access_token(self): + """Return a valid access token.""" + if not self._oauth_session.is_valid: + await self._oauth_session.async_ensure_token_valid() + + return self._oauth_session.token + + +class AlmondAgent(conversation.AbstractConversationAgent): + """Almond conversation agent.""" + + def __init__(self, api: WebAlmondAPI): + """Initialize the agent.""" + self.api = api + + async def async_process(self, text: str) -> intent.IntentResponse: + """Process a sentence.""" + response = await self.api.async_converse_text(text) + + buffer = "" + for message in response["messages"]: + if message["type"] == "text": + buffer += "\n" + message["text"] + elif message["type"] == "picture": + buffer += "\n Picture: " + message["url"] + elif message["type"] == "rdl": + buffer += ( + "\n Link: " + + message["rdl"]["displayTitle"] + + " " + + message["rdl"]["webCallback"] + ) + elif message["type"] == "choice": + buffer += "\n Choice: " + message["title"] + + intent_result = intent.IntentResponse() + intent_result.async_set_speech(buffer.strip()) + return intent_result diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py new file mode 100644 index 00000000000..d79bf6bd605 --- /dev/null +++ b/homeassistant/components/almond/config_flow.py @@ -0,0 +1,125 @@ +"""Config flow to connect with Home Assistant.""" +import asyncio +import logging + +import async_timeout +from aiohttp import ClientError +from yarl import URL +import voluptuous as vol +from pyalmond import AlmondLocalAuth, WebAlmondAPI + +from homeassistant import data_entry_flow, config_entries, core +from homeassistant.helpers import config_entry_oauth2_flow, aiohttp_client + +from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 + + +async def async_verify_local_connection(hass: core.HomeAssistant, host: str): + """Verify that a local connection works.""" + websession = aiohttp_client.async_get_clientsession(hass) + api = WebAlmondAPI(AlmondLocalAuth(host, websession)) + + try: + with async_timeout.timeout(10): + await api.async_list_apps() + + return True + except (asyncio.TimeoutError, ClientError): + return False + + +@config_entries.HANDLERS.register(DOMAIN) +class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Implementation of the Almond OAuth2 config flow.""" + + DOMAIN = DOMAIN + + host = None + hassio_discovery = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "profile user-read user-read-results user-exec-command"} + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + # Only allow 1 instance. + if self._async_current_entries(): + return self.async_abort(reason="already_setup") + + return await super().async_step_user(user_input) + + async def async_step_auth(self, user_input=None): + """Handle authorize step.""" + result = await super().async_step_auth(user_input) + + if result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP: + self.host = str(URL(result["url"]).with_path("me")) + + return result + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for the flow. + + Ok to override if you want to fetch extra info or even add another step. + """ + # pylint: disable=invalid-name + self.CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + data["type"] = TYPE_OAUTH2 + data["host"] = self.host + return self.async_create_entry(title=self.flow_impl.name, data=data) + + async def async_step_import(self, user_input: dict = None) -> dict: + """Import data.""" + # Only allow 1 instance. + if self._async_current_entries(): + return self.async_abort(reason="already_setup") + + if not await async_verify_local_connection(self.hass, user_input["host"]): + self.logger.warning( + "Aborting import of Almond because we're unable to connect" + ) + return self.async_abort(reason="cannot_connect") + + # pylint: disable=invalid-name + self.CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + return self.async_create_entry( + title="Configuration.yaml", + data={"type": TYPE_LOCAL, "host": user_input["host"]}, + ) + + async def async_step_hassio(self, user_input=None): + """Receive a Hass.io discovery.""" + if self._async_current_entries(): + return self.async_abort(reason="already_setup") + + self.hassio_discovery = user_input + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm(self, user_input=None): + """Confirm a Hass.io discovery.""" + data = self.hassio_discovery + + if user_input is not None: + return self.async_create_entry( + title=data["addon"], + data={ + "is_hassio": True, + "type": TYPE_LOCAL, + "host": f"http://{data['host']}:{data['port']}", + }, + ) + + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={"addon": data["addon"]}, + data_schema=vol.Schema({}), + ) diff --git a/homeassistant/components/almond/const.py b/homeassistant/components/almond/const.py new file mode 100644 index 00000000000..34dca28e957 --- /dev/null +++ b/homeassistant/components/almond/const.py @@ -0,0 +1,4 @@ +"""Constants for the Almond integration.""" +DOMAIN = "almond" +TYPE_OAUTH2 = "oauth2" +TYPE_LOCAL = "local" diff --git a/homeassistant/components/almond/manifest.json b/homeassistant/components/almond/manifest.json new file mode 100644 index 00000000000..44404b504f6 --- /dev/null +++ b/homeassistant/components/almond/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "almond", + "name": "Almond", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/almond", + "dependencies": ["http", "conversation"], + "codeowners": ["@gcampax", "@balloob"], + "requirements": ["pyalmond==0.0.2"] +} diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json new file mode 100644 index 00000000000..9bc4b0e1b93 --- /dev/null +++ b/homeassistant/components/almond/strings.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Almond account.", + "cannot_connect": "Unable to connect to the Almond server." + }, + "title": "Almond" + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b694af1fb71..22d36fc46c6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -9,6 +9,7 @@ FLOWS = [ "abode", "adguard", "airly", + "almond", "ambiclimate", "ambient_station", "axis", diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 87832f60739..dc3d3c91f27 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -387,17 +387,20 @@ class OAuth2Session: @property def token(self) -> dict: - """Return the current token.""" + """Return the token.""" return cast(dict, self.config_entry.data["token"]) + @property + def valid_token(self) -> bool: + """Return if token is still valid.""" + return cast(float, self.token["expires_at"]) > time.time() + async def async_ensure_token_valid(self) -> None: """Ensure that the current token is valid.""" - token = self.token - - if token["expires_at"] > time.time(): + if self.valid_token: return - new_token = await self.implementation.async_refresh_token(token) + new_token = await self.implementation.async_refresh_token(self.token) self.hass.config_entries.async_update_entry( self.config_entry, data={**self.config_entry.data, "token": new_token} diff --git a/requirements_all.txt b/requirements_all.txt index a1db7bbc1f7..54831270ff6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1089,6 +1089,9 @@ pyairvisual==3.0.1 # homeassistant.components.alarmdotcom pyalarmdotcom==0.3.2 +# homeassistant.components.almond +pyalmond==0.0.2 + # homeassistant.components.arlo pyarlo==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4aaeb373cc..280146ec45d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -390,6 +390,9 @@ pyRFXtrx==0.23 # homeassistant.components.nextbus py_nextbusnext==0.1.4 +# homeassistant.components.almond +pyalmond==0.0.2 + # homeassistant.components.arlo pyarlo==0.2.3 diff --git a/tests/components/almond/__init__.py b/tests/components/almond/__init__.py new file mode 100644 index 00000000000..717271c3a6a --- /dev/null +++ b/tests/components/almond/__init__.py @@ -0,0 +1 @@ +"""Tests for the Almond integration.""" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py new file mode 100644 index 00000000000..afbe25dff5f --- /dev/null +++ b/tests/components/almond/test_config_flow.py @@ -0,0 +1,138 @@ +"""Test the Almond config flow.""" +import asyncio + +from unittest.mock import patch + + +from homeassistant import config_entries, setup, data_entry_flow +from homeassistant.components.almond.const import DOMAIN +from homeassistant.components.almond import config_flow +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry, mock_coro + +CLIENT_ID_VALUE = "1234" +CLIENT_SECRET_VALUE = "5678" + + +async def test_import(hass): + """Test that we can import a config entry.""" + with patch("pyalmond.WebAlmondAPI.async_list_apps", side_effect=mock_coro): + assert await setup.async_setup_component( + hass, + "almond", + {"almond": {"type": "local", "host": "http://localhost:3000"}}, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "local" + assert entry.data["host"] == "http://localhost:3000" + + +async def test_import_cannot_connect(hass): + """Test that we won't import a config entry if we cannot connect.""" + with patch( + "pyalmond.WebAlmondAPI.async_list_apps", side_effect=asyncio.TimeoutError + ): + assert await setup.async_setup_component( + hass, + "almond", + {"almond": {"type": "local", "host": "http://localhost:3000"}}, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + +async def test_hassio(hass): + """Test that Hass.io can discover this integration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "hassio"}, + data={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "hassio_confirm" + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "local" + assert entry.data["host"] == "http://almond-addon:1234" + + +async def test_abort_if_existing_entry(hass): + """Check flow abort when an entry already exist.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + flow = config_flow.AlmondFlowHandler() + flow.hass = hass + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + result = await flow.async_step_import() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + result = await flow.async_step_hassio() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "almond", + { + "almond": { + "type": "oauth2", + "client_id": CLIENT_ID_VALUE, + "client_secret": CLIENT_SECRET_VALUE, + }, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "almond", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + "https://almond.stanford.edu/me/api/oauth2/authorize" + f"?response_type=code&client_id={CLIENT_ID_VALUE}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=profile+user-read+user-read-results+user-exec-command" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://almond.stanford.edu/me/api/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "oauth2" + assert entry.data["host"] == "https://almond.stanford.edu/me" From 4dc6d362240a667662d5ecca54a01dbf5969b1c8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Oct 2019 09:37:18 +0100 Subject: [PATCH 070/306] Bump pre-commit to 1.20.0 (#28313) --- 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 ad46cfa0741..18d2ece04f6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 mypy==0.740 -pre-commit==1.18.3 +pre-commit==1.20.0 pydocstyle==4.0.1 pylint==2.4.3 astroid==2.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 280146ec45d..8d4714fdeda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -11,7 +11,7 @@ flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 mypy==0.740 -pre-commit==1.18.3 +pre-commit==1.20.0 pydocstyle==4.0.1 pylint==2.4.3 astroid==2.3.2 From 756c36171d6ec0d0e8e1cb37789a7da5cc66b225 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Oct 2019 09:37:51 +0100 Subject: [PATCH 071/306] Bump youtube_dl to 2019.10.29 (#28312) --- homeassistant/components/media_extractor/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 4fd5470ebdf..bb990fc28e7 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,10 +3,10 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.10.22" + "youtube_dl==2019.10.29" ], "dependencies": [ "media_player" ], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 54831270ff6..9d35bb5a849 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2033,7 +2033,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.10.22 +youtube_dl==2019.10.29 # homeassistant.components.zengge zengge==0.2 From 502f59977af4530b38f8eef6cc037ca7242227fc Mon Sep 17 00:00:00 2001 From: Jonas Janz Date: Tue, 29 Oct 2019 11:02:25 +0100 Subject: [PATCH 072/306] Add description for arlo.update service (#28270) --- homeassistant/components/arlo/services.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/arlo/services.yaml b/homeassistant/components/arlo/services.yaml index e69de29bb2d..773bee4430a 100644 --- a/homeassistant/components/arlo/services.yaml +++ b/homeassistant/components/arlo/services.yaml @@ -0,0 +1,4 @@ +# Describes the format for available arlo services + +update: + description: Update the state for all cameras and the base station. \ No newline at end of file From c00b058e531f697912f73cac2a40a7069470a1a6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 29 Oct 2019 12:05:05 +0100 Subject: [PATCH 073/306] Cleanup not needed websocket flags for ingress (#28295) --- homeassistant/components/hassio/ingress.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4ecb9a8419f..53235f80dca 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -167,7 +167,14 @@ def _init_header( # filter flags for name, value in request.headers.items(): - if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING): + if name in ( + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_ENCODING, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, + ): continue headers[name] = value From 5592eb7709d3a318460245e2124417fb3be1b04c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 29 Oct 2019 16:30:33 +0100 Subject: [PATCH 074/306] Updated frontend to 20191025.1 (#28327) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b23d40605dd..aa7ad8b18f9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191025.0" + "home-assistant-frontend==20191025.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 87878b49615..1933448edda 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.23 -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9d35bb5a849..eb1db77263e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -649,7 +649,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d4714fdeda..2f44dab78d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 1dfb67f0c5adee5a751fd162ccd085027057c39a Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 29 Oct 2019 20:16:05 +0100 Subject: [PATCH 075/306] Bump pytest to 5.2.2 (#28230) --- 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 18d2ece04f6..f2935a423bf 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,6 +18,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.1 +pytest==5.2.2 requests_mock==1.7.0 responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f44dab78d8..8166b9713f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.1 +pytest==5.2.2 requests_mock==1.7.0 responses==0.10.6 From 3a9e3ce857160c79d7c8b9e2395d30ea10875e06 Mon Sep 17 00:00:00 2001 From: Renaud Martinet Date: Tue, 29 Oct 2019 20:17:49 +0100 Subject: [PATCH 076/306] Add services description for sabnzbd component (#28252) --- homeassistant/components/sabnzbd/services.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml index e69de29bb2d..654cb50fa1e 100644 --- a/homeassistant/components/sabnzbd/services.yaml +++ b/homeassistant/components/sabnzbd/services.yaml @@ -0,0 +1,11 @@ +pause: + description: Pauses downloads. +resume: + description: Resumes downloads. +set_speed: + description: Sets the download speed limit. + fields: + speed: + description: Speed limit. If specified as a number with no units, will be interpreted as a percent. If units are provided (e.g., 500K) will be interpreted absolutely. + example: 100 + default: 100 From 6d734a714e6734eece955cf89ac5bf68f5e2595e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 29 Oct 2019 22:17:09 +0100 Subject: [PATCH 077/306] Clean up Xiaomi Air Quality Monitor support (cgllc.airmonitor.b1) (#28301) * Clean up Xiaomi Air Quality Monitor support (cgllc.airmonitor.b1) * Remove unused variable * Provide a proper unique_id * Incorporate review * Wrap the method that cause the exception * Undo mistakenly changed file. Fixed in the separate PR. --- .../components/xiaomi_miio/air_quality.py | 102 ++++-------------- 1 file changed, 22 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index e96ed074002..b80906aa0cb 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -1,26 +1,20 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" -from miio import AirQualityMonitor, DeviceException +from miio import AirQualityMonitor, Device, DeviceException import voluptuous as vol from homeassistant.components.air_quality import ( AirQualityEntity, PLATFORM_SCHEMA, _LOGGER, - ATTR_PM_2_5, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, ATTR_TEMPERATURE +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor" -DATA_KEY = "air_quality.xiaomi_miio" ATTR_CO2E = "carbon_dioxide_equivalent" -ATTR_HUMIDITY = "relative_humidity" ATTR_TVOC = "total_volatile_organic_compounds" -ATTR_MANUFACTURER = "manufacturer" -ATTR_MODEL = "model" -ATTR_SW_VERSION = "sw_version" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -32,74 +26,62 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PROP_TO_ATTR = { "carbon_dioxide_equivalent": ATTR_CO2E, - "relative_humidity": ATTR_HUMIDITY, - "particulate_matter_2_5": ATTR_PM_2_5, - "temperature": ATTR_TEMPERATURE, "total_volatile_organic_compounds": ATTR_TVOC, - "manufacturer": ATTR_MANUFACTURER, - "model": ATTR_MODEL, - "sw_version": ATTR_SW_VERSION, } async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sensor from config.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - host = config.get(CONF_HOST) - token = config.get(CONF_TOKEN) - name = config.get(CONF_NAME) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - try: - device = AirMonitorB1(name, AirQualityMonitor(host, token, model=None)) + miio_device = Device(host, token) + try: + device_info = await hass.async_add_executor_job(miio_device.info) except DeviceException: raise PlatformNotReady - hass.data[DATA_KEY][host] = device + model = device_info.model + unique_id = f"{model}-{device_info.mac_address}" + _LOGGER.debug( + "%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version, + ) + device = AirMonitorB1(name, AirQualityMonitor(host, token, model), unique_id) + async_add_entities([device], update_before_add=True) class AirMonitorB1(AirQualityEntity): """Air Quality class for Xiaomi cgllc.airmonitor.b1 device.""" - def __init__(self, name, device): + def __init__(self, name, device, unique_id): """Initialize the entity.""" self._name = name self._device = device + self._unique_id = unique_id self._icon = "mdi:cloud" - self._manufacturer = "Xiaomi" self._unit_of_measurement = "μg/m3" - self._model = None - self._mac_address = None - self._sw_version = None self._carbon_dioxide_equivalent = None - self._relative_humidity = None self._particulate_matter_2_5 = None - self._temperature = None self._total_volatile_organic_compounds = None async def async_update(self): """Fetch state from the miio device.""" try: - if self._model is None: - info = await self.hass.async_add_executor_job(self._device.info) - self._model = info.model - self._mac_address = info.mac_address - self._sw_version = info.firmware_version - state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) self._carbon_dioxide_equivalent = state.co2e - self._relative_humidity = round(state.humidity, 1) self._particulate_matter_2_5 = round(state.pm25, 1) - self._temperature = round(state.temperature, 1) self._total_volatile_organic_compounds = round(state.tvoc, 3) except DeviceException as ex: @@ -110,68 +92,33 @@ class AirMonitorB1(AirQualityEntity): """Return the name of this entity, if any.""" return self._name - @property - def device(self): - """Return the name of this entity, if any.""" - return self._device - @property def icon(self): """Return the icon to use for device if any.""" return self._icon - @property - def manufacturer(self): - """Return the manufacturer version.""" - return self._manufacturer - - @property - def model(self): - """Return the device model.""" - return self._model - - @property - def sw_version(self): - """Return the software version.""" - return self._sw_version - - @property - def mac_address(self): - """Return the mac address.""" - return self._mac_address - @property def unique_id(self): """Return the unique ID.""" - return f"{self._model}-{self._mac_address}" + return self._unique_id @property def carbon_dioxide_equivalent(self): """Return the CO2e (carbon dioxide equivalent) level.""" return self._carbon_dioxide_equivalent - @property - def relative_humidity(self): - """Return the humidity percentage.""" - return self._relative_humidity - @property def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" return self._particulate_matter_2_5 - @property - def temperature(self): - """Return the temperature in °C.""" - return self._temperature - @property def total_volatile_organic_compounds(self): """Return the total volatile organic compounds.""" return self._total_volatile_organic_compounds @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes.""" data = {} @@ -186,8 +133,3 @@ class AirMonitorB1(AirQualityEntity): def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement - - @property - def state(self): - """Return the current state.""" - return self._particulate_matter_2_5 From e1eab214ad67a88a2e4a25a1ee82fe1a8c502ecf Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 29 Oct 2019 21:29:39 +0000 Subject: [PATCH 078/306] allow multiple heaters per incomfort gateway (#28324) * add multiple heaters per gateway * bump client to handle the above --- homeassistant/components/incomfort/__init__.py | 5 +++-- homeassistant/components/incomfort/binary_sensor.py | 7 ++++--- homeassistant/components/incomfort/climate.py | 6 ++++-- homeassistant/components/incomfort/manifest.json | 2 +- homeassistant/components/incomfort/sensor.py | 10 ++++------ homeassistant/components/incomfort/water_heater.py | 4 ++-- requirements_all.txt | 2 +- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index adf57e35093..bb115650061 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -44,12 +44,13 @@ async def async_setup(hass, hass_config): ) try: - heater = incomfort_data["heater"] = list(await client.heaters)[0] + heaters = incomfort_data["heaters"] = list(await client.heaters) except ClientResponseError as err: _LOGGER.warning("Setup failed, check your configuration, message is: %s", err) return False - await heater.update() + for heater in heaters: + await heater.update() for platform in ["water_heater", "binary_sensor", "sensor", "climate"]: hass.async_create_task( diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index b5dbd8e223d..150515cbbf5 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -11,9 +11,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - async_add_entities( - [IncomfortFailed(hass.data[DOMAIN]["client"], hass.data[DOMAIN]["heater"])] - ) + client = hass.data[DOMAIN]["client"] + heaters = hass.data[DOMAIN]["heaters"] + + async_add_entities([IncomfortFailed(client, h) for h in heaters]) class IncomfortFailed(IncomfortChild, BinarySensorDevice): diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 95ccf186372..23bda6b2fdf 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -17,9 +17,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return client = hass.data[DOMAIN]["client"] - heater = hass.data[DOMAIN]["heater"] + heaters = hass.data[DOMAIN]["heaters"] - async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms]) + async_add_entities( + [InComfortClimate(client, h, r) for h in heaters for r in h.rooms] + ) class InComfortClimate(IncomfortChild, ClimateDevice): diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 4bdf43f8957..45365c7e354 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -3,7 +3,7 @@ "name": "Intergas InComfort/Intouch Lan2RF gateway", "documentation": "https://www.home-assistant.io/integrations/incomfort", "requirements": [ - "incomfort-client==0.3.5" + "incomfort-client==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index f3170b7b9bb..4164225b0d7 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -29,14 +29,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return client = hass.data[DOMAIN]["client"] - heater = hass.data[DOMAIN]["heater"] + heaters = hass.data[DOMAIN]["heaters"] async_add_entities( - [ - IncomfortPressure(client, heater, INCOMFORT_PRESSURE), - IncomfortTemperature(client, heater, INCOMFORT_HEATER_TEMP), - IncomfortTemperature(client, heater, INCOMFORT_TAP_TEMP), - ] + [IncomfortPressure(client, h, INCOMFORT_PRESSURE) for h in heaters] + + [IncomfortTemperature(client, h, INCOMFORT_HEATER_TEMP) for h in heaters] + + [IncomfortTemperature(client, h, INCOMFORT_TAP_TEMP) for h in heaters] ) diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 0015107b40f..9096a7cb72c 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -22,9 +22,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return client = hass.data[DOMAIN]["client"] - heater = hass.data[DOMAIN]["heater"] + heaters = hass.data[DOMAIN]["heaters"] - async_add_entities([IncomfortWaterHeater(client, heater)]) + async_add_entities([IncomfortWaterHeater(client, h) for h in heaters]) class IncomfortWaterHeater(IncomfortEntity, WaterHeaterDevice): diff --git a/requirements_all.txt b/requirements_all.txt index eb1db77263e..51f7d0816c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -694,7 +694,7 @@ iglo==1.2.7 ihcsdk==2.3.0 # homeassistant.components.incomfort -incomfort-client==0.3.5 +incomfort-client==0.4.0 # homeassistant.components.influxdb influxdb==5.2.3 From e700384cceba83f7f5e9222eee6dac2161585000 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 30 Oct 2019 00:32:11 +0000 Subject: [PATCH 079/306] [ci skip] Translation update --- .../components/almond/.translations/ca.json | 9 ++++++ .../components/almond/.translations/en.json | 15 +++++---- .../components/almond/.translations/fr.json | 9 ++++++ .../components/almond/.translations/lb.json | 9 ++++++ .../components/almond/.translations/ru.json | 9 ++++++ .../almond/.translations/zh-Hant.json | 9 ++++++ .../components/cover/.translations/lb.json | 4 ++- .../components/deconz/.translations/fr.json | 2 +- .../device_tracker/.translations/lb.json | 8 +++++ .../components/glances/.translations/fr.json | 2 +- .../huawei_lte/.translations/fr.json | 2 +- .../media_player/.translations/lb.json | 11 +++++++ .../components/met/.translations/fr.json | 2 +- .../components/plex/.translations/fr.json | 2 +- .../components/ps4/.translations/fr.json | 2 +- .../components/sensor/.translations/nl.json | 32 +++++++++---------- .../components/somfy/.translations/lb.json | 5 +++ 17 files changed, 102 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/almond/.translations/ca.json create mode 100644 homeassistant/components/almond/.translations/fr.json create mode 100644 homeassistant/components/almond/.translations/lb.json create mode 100644 homeassistant/components/almond/.translations/ru.json create mode 100644 homeassistant/components/almond/.translations/zh-Hant.json create mode 100644 homeassistant/components/device_tracker/.translations/lb.json create mode 100644 homeassistant/components/media_player/.translations/lb.json diff --git a/homeassistant/components/almond/.translations/ca.json b/homeassistant/components/almond/.translations/ca.json new file mode 100644 index 00000000000..cf4618d2272 --- /dev/null +++ b/homeassistant/components/almond/.translations/ca.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.", + "cannot_connect": "No es pot connectar amb el servidor d'Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json index cc48b1c28eb..89134cbb170 100644 --- a/homeassistant/components/almond/.translations/en.json +++ b/homeassistant/components/almond/.translations/en.json @@ -1,8 +1,9 @@ { - "config": { - "abort": { - "already_setup": "You can only configure one Almond account." - }, - "title": "Almond" - } -} + "config": { + "abort": { + "already_setup": "You can only configure one Almond account.", + "cannot_connect": "Unable to connect to the Almond server." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/fr.json b/homeassistant/components/almond/.translations/fr.json new file mode 100644 index 00000000000..b304a596b3a --- /dev/null +++ b/homeassistant/components/almond/.translations/fr.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Almond", + "cannot_connect": "Impossible de se connecter au serveur Almond" + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/lb.json b/homeassistant/components/almond/.translations/lb.json new file mode 100644 index 00000000000..f74874d283a --- /dev/null +++ b/homeassistant/components/almond/.translations/lb.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Almond Kont konfigur\u00e9ieren.", + "cannot_connect": "Kann sech net mam Almond Server verbannen." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ru.json b/homeassistant/components/almond/.translations/ru.json new file mode 100644 index 00000000000..b513d5b28d7 --- /dev/null +++ b/homeassistant/components/almond/.translations/ru.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/zh-Hant.json b/homeassistant/components/almond/.translations/zh-Hant.json new file mode 100644 index 00000000000..c84b2dd432b --- /dev/null +++ b/homeassistant/components/almond/.translations/zh-Hant.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Almond \u5e33\u865f\u3002", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002" + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json index b0c9e1d0d4c..4f7a898c772 100644 --- a/homeassistant/components/cover/.translations/lb.json +++ b/homeassistant/components/cover/.translations/lb.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} ass zou", "is_closing": "{entity_name} g\u00ebtt zougemaach", "is_open": "{entity_name} ass op", - "is_opening": "{entity_name} g\u00ebtt opgemaach" + "is_opening": "{entity_name} g\u00ebtt opgemaach", + "is_position": "{entity_name} positioun ass", + "is_tilt_position": "{entity_name} kipp positioun ass" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index d1fc7fa7286..3b29dbf486d 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -29,7 +29,7 @@ "title": "Initialiser la passerelle deCONZ" }, "link": { - "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer aupr\u00e8s de Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", + "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer avec Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres avanc\u00e9s du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", "title": "Lien vers deCONZ" }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/lb.json b/homeassistant/components/device_tracker/.translations/lb.json new file mode 100644 index 00000000000..98a066ef8e8 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/lb.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} ass doheem", + "is_not_home": "{entity_name} ass net doheem" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/fr.json b/homeassistant/components/glances/.translations/fr.json index 0391012c4cd..b65df092b32 100644 --- a/homeassistant/components/glances/.translations/fr.json +++ b/homeassistant/components/glances/.translations/fr.json @@ -14,7 +14,7 @@ "name": "Nom", "password": "Mot de passe", "port": "Port", - "ssl": "V\u00e9rifier la certification du syst\u00e8me", + "ssl": "Utiliser SSL / TLS pour se connecter au syst\u00e8me Glances", "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me", "version": "Glances API Version (2 ou 3)" diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index 19f33305356..e0394d525d4 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -31,7 +31,7 @@ "init": { "data": { "recipient": "Destinataires des notifications SMS", - "track_new_devices": "Suivre de nouveaux appareils" + "track_new_devices": "Suivre les nouveaux appareils" } } } diff --git a/homeassistant/components/media_player/.translations/lb.json b/homeassistant/components/media_player/.translations/lb.json new file mode 100644 index 00000000000..99b6f12fd66 --- /dev/null +++ b/homeassistant/components/media_player/.translations/lb.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} waart", + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", + "is_paused": "{entity_name} ass paus\u00e9iert", + "is_playing": "{entity_name} spillt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/fr.json b/homeassistant/components/met/.translations/fr.json index 7100cb5e4a7..164cb13967b 100644 --- a/homeassistant/components/met/.translations/fr.json +++ b/homeassistant/components/met/.translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + "name_exists": "Emplacement d\u00e9j\u00e0 existant" }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index c06d314ec72..2eef7a5e9a2 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -42,7 +42,7 @@ "manual_setup": "Installation manuelle", "token": "Jeton plex" }, - "description": "Entrez un jeton Plex pour la configuration automatique.", + "description": "Continuez pour autoriser plex.tv ou configurez manuellement un serveur.", "title": "Connecter un serveur Plex" } }, diff --git a/homeassistant/components/ps4/.translations/fr.json b/homeassistant/components/ps4/.translations/fr.json index 991222d45be..5c49723657c 100644 --- a/homeassistant/components/ps4/.translations/fr.json +++ b/homeassistant/components/ps4/.translations/fr.json @@ -25,7 +25,7 @@ "name": "Nom", "region": "R\u00e9gion" }, - "description": "Entrez vos informations PlayStation 4. Pour \"Code PIN\", acc\u00e9dez \u00e0 \"Param\u00e8tres\" sur votre console PlayStation 4. Ensuite, acc\u00e9dez \u00e0 \"Param\u00e8tres de connexion de l'application mobile\" et s\u00e9lectionnez \"Ajouter un p\u00e9riph\u00e9rique\". Entrez le code PIN qui est affich\u00e9.", + "description": "Entrez vos informations PlayStation 4. Pour \"Code PIN\", acc\u00e9dez \u00e0 \"Param\u00e8tres\" sur votre console PlayStation 4. Ensuite, acc\u00e9dez \u00e0 \"Param\u00e8tres de connexion de l'application mobile\" et s\u00e9lectionnez \"Ajouter un p\u00e9riph\u00e9rique\". Entrez le code PIN qui est affich\u00e9. Consultez la documentation pour plus d'informations.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json index 796e9c97071..03eff2a7f6d 100644 --- a/homeassistant/components/sensor/.translations/nl.json +++ b/homeassistant/components/sensor/.translations/nl.json @@ -2,25 +2,25 @@ "device_automation": { "condition_type": { "is_battery_level": "Huidige batterijniveau {entity_name}", - "is_humidity": "{entity_name} vochtigheidsgraad", - "is_illuminance": "{entity_name} verlichtingssterkte", - "is_power": "{entity_name}\nvermogen", - "is_pressure": "{entity_name} druk", - "is_signal_strength": "{entity_name} signaalsterkte", - "is_temperature": "{entity_name} temperatuur", - "is_timestamp": "{entity_name} tijdstip", + "is_humidity": "Huidige {entity_name} vochtigheidsgraad", + "is_illuminance": "Huidige {entity_name} verlichtingssterkte", + "is_power": "Huidige {entity_name}\nvermogen", + "is_pressure": "Huidige {entity_name} druk", + "is_signal_strength": "Huidige {entity_name} signaalsterkte", + "is_temperature": "Huidige {entity_name} temperatuur", + "is_timestamp": "Huidige {entity_name} tijdstip", "is_value": "Huidige {entity_name} waarde" }, "trigger_type": { - "battery_level": "{entity_name} batterijniveau", - "humidity": "{entity_name} vochtigheidsgraad", - "illuminance": "{entity_name} verlichtingssterkte", - "power": "{entity_name} vermogen", - "pressure": "{entity_name} druk", - "signal_strength": "{entity_name} signaalsterkte", - "temperature": "{entity_name} temperatuur", - "timestamp": "{entity_name} tijdstip", - "value": "{entity_name} waarde" + "battery_level": "{entity_name} batterijniveau gewijzigd", + "humidity": "{entity_name} vochtigheidsgraad gewijzigd", + "illuminance": "{entity_name} verlichtingssterkte gewijzigd", + "power": "{entity_name} vermogen gewijzigd", + "pressure": "{entity_name} druk gewijzigd", + "signal_strength": "{entity_name} signaalsterkte gewijzigd", + "temperature": "{entity_name} temperatuur gewijzigd", + "timestamp": "{entity_name} tijdstip gewijzigd", + "value": "{entity_name} waarde gewijzigd" } } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/lb.json b/homeassistant/components/somfy/.translations/lb.json index 62f58829241..0a1cfa83250 100644 --- a/homeassistant/components/somfy/.translations/lb.json +++ b/homeassistant/components/somfy/.translations/lb.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Erfollegr\u00e4ich mat Somfy authentifiz\u00e9iert." }, + "step": { + "pick_implementation": { + "title": "Wielt Authentifikatiouns Method aus" + } + }, "title": "Somfy" } } \ No newline at end of file From 24c29f922752bc734b19a7bf10a66be996bea870 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 Oct 2019 20:34:03 -0700 Subject: [PATCH 080/306] Add OAuth2 config flow scaffold (#28220) * Add OAuth2 scaffold * Generate integration if non-existing domain specified * Update URL --- homeassistant/const.py | 2 + script/scaffold/__main__.py | 50 ++++-- script/scaffold/docs.py | 130 +++++++------- script/scaffold/gather_info.py | 170 ++++++++---------- script/scaffold/generate.py | 78 ++++---- script/scaffold/model.py | 7 + .../config_flow/integration/__init__.py | 49 +++++ .../integration/__init__.py | 49 +++++ .../integration/__init__.py | 94 ++++++++++ .../config_flow_oauth2/integration/api.py | 58 ++++++ .../integration/config_flow.py | 23 +++ .../tests/test_config_flow.py | 60 +++++++ .../integration/integration/__init__.py | 6 +- 13 files changed, 567 insertions(+), 209 deletions(-) create mode 100644 script/scaffold/templates/config_flow/integration/__init__.py create mode 100644 script/scaffold/templates/config_flow_discovery/integration/__init__.py create mode 100644 script/scaffold/templates/config_flow_oauth2/integration/__init__.py create mode 100644 script/scaffold/templates/config_flow_oauth2/integration/api.py create mode 100644 script/scaffold/templates/config_flow_oauth2/integration/config_flow.py create mode 100644 script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py diff --git a/homeassistant/const.py b/homeassistant/const.py index e1e9757dd02..449e7a90087 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -40,6 +40,8 @@ CONF_BELOW = "below" CONF_BINARY_SENSORS = "binary_sensors" CONF_BLACKLIST = "blacklist" CONF_BRIGHTNESS = "brightness" +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" CONF_CODE = "code" CONF_COLOR_TEMP = "color_temp" CONF_COMMAND = "command" diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 2258840f430..78490b84ba3 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -48,34 +48,46 @@ def main(): args = get_arguments() info = gather_info.gather_info(args) + print() - generate.generate(args.template, info) + # If we are calling scaffold on a non-existing integration, + # We're going to first make it. If we're making an integration, + # we will also make a config flow to go with it. - # If creating new integration, create config flow too - if args.template == "integration": - if info.authentication or not info.discoverable: - template = "config_flow" - else: - template = "config_flow_discovery" + if info.is_new: + generate.generate("integration", info) - generate.generate(template, info) + # If it's a new integration and it's not a config flow, + # create a config flow too. + if not args.template.startswith("config_flow"): + if info.oauth2: + template = "config_flow_oauth2" + elif info.authentication or not info.discoverable: + template = "config_flow" + else: + template = "config_flow_discovery" + + generate.generate(template, info) + + # If we wanted a new integration, we've already done our work. + if args.template != "integration": + generate.generate(args.template, info) + + pipe_null = "" if args.develop else "> /dev/null" print("Running hassfest to pick up new information.") - subprocess.run("python -m script.hassfest", shell=True) + subprocess.run(f"python -m script.hassfest {pipe_null}", shell=True) print() - print("Running tests") - print(f"$ pytest -vvv tests/components/{info.domain}") - if ( - subprocess.run( - f"pytest -vvv tests/components/{info.domain}", shell=True - ).returncode - != 0 - ): - return 1 + print("Running gen_requirements_all to pick up new information.") + subprocess.run(f"python -m script.gen_requirements_all {pipe_null}", shell=True) print() - print(f"Done!") + if args.develop: + print("Running tests") + print(f"$ pytest -vvv tests/components/{info.domain}") + subprocess.run(f"pytest -vvv tests/components/{info.domain}", shell=True) + print() docs.print_relevant_docs(args.template, info) diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index bb119c0e42e..5df663fec0b 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -2,72 +2,76 @@ from .model import Info +DATA = { + "config_flow": { + "title": "Config Flow", + "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html", + }, + "config_flow_discovery": { + "title": "Discoverable Config Flow", + "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#discoverable-integrations-that-require-no-authentication", + }, + "config_flow_oauth2": { + "title": "OAuth2 Config Flow", + "docs": "https://developers.home-assistant.io/docs/en/next/config_entries_config_flow_handler.html#configuration-via-oauth2", + }, + "device_action": { + "title": "Device Action", + "docs": "https://developers.home-assistant.io/docs/en/device_automation_action.html", + }, + "device_condition": { + "title": "Device Condition", + "docs": "https://developers.home-assistant.io/docs/en/device_automation_condition.html", + }, + "device_trigger": { + "title": "Device Trigger", + "docs": "https://developers.home-assistant.io/docs/en/device_automation_trigger.html", + }, + "integration": { + "title": "Integration", + "docs": "https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html", + }, + "reproduce_state": { + "title": "Reproduce State", + "docs": "https://developers.home-assistant.io/docs/en/reproduce_state_index.html", + "extra": "You will now need to update the code to make sure that every attribute that can occur in the state will cause the right service to be called.", + }, +} + + def print_relevant_docs(template: str, info: Info) -> None: """Print relevant docs.""" - if template == "integration": + data = DATA[template] + + print() + print("**************************") + print() + print() + print(f"{data['title']} code has been generated") + print() + if info.files_added: + print("Added the following files:") + for file in info.files_added: + print(f"- {file}") + print() + + if info.tests_added: + print("Added the following tests:") + for file in info.tests_added: + print(f"- {file}") + print() + + if info.examples_added: print( - f""" -Your integration has been created at {info.integration_dir} . Next step is to fill in the blanks for the code marked with TODO. - -For a breakdown of each file, check the developer documentation at: -https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html -""" + "Because some files already existed, we added the following example files. Please copy the relevant code to the existing files." ) + for file in info.examples_added: + print(f"- {file}") + print() - elif template == "config_flow": - print( - f""" -The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO. -""" - ) + print( + f"The next step is to look at the files and deal with all areas marked as TODO." + ) - elif template == "reproduce_state": - print( - f""" -Reproduce state code has been added to the {info.domain} integration: - - {info.integration_dir / "reproduce_state.py"} - - {info.tests_dir / "test_reproduce_state.py"} - -You will now need to update the code to make sure that every attribute -that can occur in the state will cause the right service to be called. -""" - ) - - elif template == "device_trigger": - print( - f""" -Device trigger base has been added to the {info.domain} integration: - - {info.integration_dir / "device_trigger.py"} - - {info.integration_dir / "strings.json"} (translations) - - {info.tests_dir / "test_device_trigger.py"} - -You will now need to update the code to make sure that relevant triggers -are exposed. -""" - ) - - elif template == "device_condition": - print( - f""" -Device condition base has been added to the {info.domain} integration: - - {info.integration_dir / "device_condition.py"} - - {info.integration_dir / "strings.json"} (translations) - - {info.tests_dir / "test_device_condition.py"} - -You will now need to update the code to make sure that relevant condtions -are exposed. -""" - ) - - elif template == "device_action": - print( - f""" -Device action base has been added to the {info.domain} integration: - - {info.integration_dir / "device_action.py"} - - {info.integration_dir / "strings.json"} (translations) - - {info.tests_dir / "test_device_action.py"} - -You will now need to update the code to make sure that relevant services -are exposed as actions. -""" - ) + if "extra" in data: + print(data["extra"]) diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index a7263daaf41..12cb319d188 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -13,36 +13,14 @@ CHECK_EMPTY = ["Cannot be empty", lambda value: value] def gather_info(arguments) -> Info: """Gather info.""" - existing = arguments.template != "integration" - - if arguments.develop: + if arguments.integration: + info = {"domain": arguments.integration} + elif arguments.develop: print("Running in developer mode. Automatically filling in info.") print() - - if existing: - if arguments.develop: - return _load_existing_integration("develop") - - if arguments.integration: - return _load_existing_integration(arguments.integration) - - return gather_existing_integration() - - if arguments.develop: - return Info( - domain="develop", - name="Develop Hub", - codeowner="@developer", - requirement="aiodevelop==1.2.3", - ) - - return gather_new_integration() - - -def gather_new_integration() -> Info: - """Gather info about new integration from user.""" - return Info( - **_gather_info( + info = {"domain": "develop"} + else: + info = _gather_info( { "domain": { "prompt": "What is the domain?", @@ -52,84 +30,87 @@ def gather_new_integration() -> Info: "Domains cannot contain spaces or special characters.", lambda value: value == slugify(value), ], - [ - "There already is an integration with this domain.", - lambda value: not (COMPONENT_DIR / value).exists(), - ], ], - }, - "name": { - "prompt": "What is the name of your integration?", - "validators": [CHECK_EMPTY], - }, - "codeowner": { - "prompt": "What is your GitHub handle?", - "validators": [ - CHECK_EMPTY, - [ - 'GitHub handles need to start with an "@"', - lambda value: value.startswith("@"), - ], - ], - }, - "requirement": { - "prompt": "What PyPI package and version do you depend on? Leave blank for none.", - "validators": [ - [ - "Versions should be pinned using '=='.", - lambda value: not value or "==" in value, - ] - ], - }, + } + } + ) + + info["is_new"] = not (COMPONENT_DIR / info["domain"] / "manifest.json").exists() + + if not info["is_new"]: + return _load_existing_integration(info["domain"]) + + if arguments.develop: + info.update( + { + "name": "Develop Hub", + "codeowner": "@developer", + "requirement": "aiodevelop==1.2.3", + "oauth2": True, + } + ) + else: + info.update(gather_new_integration(arguments.template == "integration")) + + return Info(**info) + + +YES_NO = { + "validators": [["Type either 'yes' or 'no'", lambda value: value in ("yes", "no")]], + "convertor": lambda value: value == "yes", +} + + +def gather_new_integration(determine_auth: bool) -> Info: + """Gather info about new integration from user.""" + fields = { + "name": { + "prompt": "What is the name of your integration?", + "validators": [CHECK_EMPTY], + }, + "codeowner": { + "prompt": "What is your GitHub handle?", + "validators": [ + CHECK_EMPTY, + [ + 'GitHub handles need to start with an "@"', + lambda value: value.startswith("@"), + ], + ], + }, + "requirement": { + "prompt": "What PyPI package and version do you depend on? Leave blank for none.", + "validators": [ + [ + "Versions should be pinned using '=='.", + lambda value: not value or "==" in value, + ] + ], + }, + } + + if determine_auth: + fields.update( + { "authentication": { "prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)", "default": "yes", - "validators": [ - [ - "Type either 'yes' or 'no'", - lambda value: value in ("yes", "no"), - ] - ], - "convertor": lambda value: value == "yes", + **YES_NO, }, "discoverable": { "prompt": "Is the device/service discoverable on the local network? (yes/no)", "default": "no", - "validators": [ - [ - "Type either 'yes' or 'no'", - lambda value: value in ("yes", "no"), - ] - ], - "convertor": lambda value: value == "yes", + **YES_NO, + }, + "oauth2": { + "prompt": "Can the user authenticate the device using OAuth2? (yes/no)", + "default": "no", + **YES_NO, }, } ) - ) - -def gather_existing_integration() -> Info: - """Gather info about existing integration from user.""" - answers = _gather_info( - { - "domain": { - "prompt": "What is the domain?", - "validators": [ - CHECK_EMPTY, - [ - "Domains cannot contain spaces or special characters.", - lambda value: value == slugify(value), - ], - [ - "This integration does not exist.", - lambda value: (COMPONENT_DIR / value).exists(), - ], - ], - } - } - ) - - return _load_existing_integration(answers["domain"]) + return _gather_info(fields) def _load_existing_integration(domain) -> Info: @@ -179,5 +160,4 @@ def _gather_info(fields) -> dict: value = info["convertor"](value) answers[key] = value - print() return answers diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index e16316fd76b..a04cdb3ef5e 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -1,7 +1,6 @@ """Generate an integration.""" from pathlib import Path -from .error import ExitApp from .model import Info TEMPLATE_DIR = Path(__file__).parent / "templates" @@ -11,8 +10,6 @@ TEMPLATE_TESTS = TEMPLATE_DIR / "tests" def generate(template: str, info: Info) -> None: """Generate a template.""" - _validate(template, info) - print(f"Scaffolding {template} for the {info.domain} integration...") _ensure_tests_dir_exists(info) _generate(TEMPLATE_DIR / template / "integration", info.integration_dir, info) @@ -21,13 +18,6 @@ def generate(template: str, info: Info) -> None: print() -def _validate(template, info): - """Validate we can run this task.""" - if template == "config_flow": - if (info.integration_dir / "config_flow.py").exists(): - raise ExitApp(f"Integration {info.domain} already has a config flow.") - - def _generate(src_dir, target_dir, info: Info) -> None: """Generate an integration.""" replaces = {"NEW_DOMAIN": info.domain, "NEW_NAME": info.name} @@ -42,6 +32,20 @@ def _generate(src_dir, target_dir, info: Info) -> None: content = content.replace(to_search, to_replace) target_file = target_dir / source_file.relative_to(src_dir) + + # If the target file exists, create our template as EXAMPLE_. + # Exception: If we are creating a new integration, we can end up running integration base + # and a config flows on top of one another. In that case, we want to override the files. + if not info.is_new and target_file.exists(): + new_name = f"EXAMPLE_{target_file.name}" + print(f"File {target_file} already exists, creating {new_name} instead.") + target_file = target_file.parent / new_name + info.examples_added.add(target_file) + elif src_dir.name == "integration": + info.files_added.add(target_file) + else: + info.tests_added.add(target_file) + print(f"Writing {target_file}") target_file.write_text(content) @@ -58,6 +62,11 @@ def _ensure_tests_dir_exists(info: Info) -> None: ) +def _append(path: Path, text): + """Append some text to a path.""" + path.write_text(path.read_text() + text) + + def _custom_tasks(template, info) -> None: """Handle custom tasks for templates.""" if template == "integration": @@ -68,7 +77,7 @@ def _custom_tasks(template, info) -> None: info.update_manifest(**changes) - if template == "device_trigger": + elif template == "device_trigger": info.update_strings( device_automation={ **info.strings().get("device_automation", {}), @@ -79,7 +88,7 @@ def _custom_tasks(template, info) -> None: } ) - if template == "device_condition": + elif template == "device_condition": info.update_strings( device_automation={ **info.strings().get("device_automation", {}), @@ -90,7 +99,7 @@ def _custom_tasks(template, info) -> None: } ) - if template == "device_action": + elif template == "device_action": info.update_strings( device_automation={ **info.strings().get("device_automation", {}), @@ -101,7 +110,7 @@ def _custom_tasks(template, info) -> None: } ) - if template == "config_flow": + elif template == "config_flow": info.update_manifest(config_flow=True) info.update_strings( config={ @@ -118,7 +127,7 @@ def _custom_tasks(template, info) -> None: } ) - if template == "config_flow_discovery": + elif template == "config_flow_discovery": info.update_manifest(config_flow=True) info.update_strings( config={ @@ -136,19 +145,28 @@ def _custom_tasks(template, info) -> None: } ) - if template in ("config_flow", "config_flow_discovery"): - init_file = info.integration_dir / "__init__.py" - init_file.write_text( - init_file.read_text() - + """ - -async def async_setup_entry(hass, entry): - \"\"\"Set up a config entry for NEW_NAME.\"\"\" - # TODO forward the entry for each platform that you want to set up. - # hass.async_create_task( - # hass.config_entries.async_forward_entry_setup(entry, "media_player") - # ) - - return True -""" + elif template == "config_flow_oauth2": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "title": info.name, + "step": { + "pick_implementation": {"title": "Pick Authentication Method"} + }, + "abort": { + "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": f"Successfully authenticated with {info.name}." + }, + } + ) + _append( + info.integration_dir / "const.py", + """ + +# TODO Update with your own urls +OAUTH2_AUTHORIZE = "https://www.example.com/auth/authorize" +OAUTH2_TOKEN = "https://www.example.com/auth/token" +""", ) diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 68ab771122e..bfbcfa52544 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -1,6 +1,7 @@ """Models for scaffolding.""" import json from pathlib import Path +from typing import Set import attr @@ -13,10 +14,16 @@ class Info: domain: str = attr.ib() name: str = attr.ib() + is_new: bool = attr.ib() codeowner: str = attr.ib(default=None) requirement: str = attr.ib(default=None) authentication: str = attr.ib(default=None) discoverable: str = attr.ib(default=None) + oauth2: str = attr.ib(default=None) + + files_added: Set[Path] = attr.ib(factory=set) + tests_added: Set[Path] = attr.ib(factory=set) + examples_added: Set[Path] = attr.ib(factory=set) @property def integration_dir(self) -> Path: diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py new file mode 100644 index 00000000000..403453a1f6b --- /dev/null +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -0,0 +1,49 @@ +"""The NEW_NAME integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the NEW_NAME component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Somfy from a config entry.""" + # TODO Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(…) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py new file mode 100644 index 00000000000..403453a1f6b --- /dev/null +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -0,0 +1,49 @@ +"""The NEW_NAME integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the NEW_NAME component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Somfy from a config entry.""" + # TODO Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(…) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py new file mode 100644 index 00000000000..43b4c6f31cd --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -0,0 +1,94 @@ +"""The NEW_NAME integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.helpers import ( + config_validation as cv, + config_entry_oauth2_flow, + aiohttp_client, +) +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from . import api, config_flow + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the NEW_NAME component.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + config_flow.OAuth2FlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ), + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Somfy from a config entry.""" + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + # If using a requests-based API lib + hass.data[DOMAIN][entry.entry_id] = api.ConfigEntryAuth(hass, entry, session) + + # If using an aiohttp-based API lib + hass.data[DOMAIN][entry.entry_id] = api.AsyncConfigEntryAuth( + aiohttp_client.async_get_clientsession(hass), session + ) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py new file mode 100644 index 00000000000..c5aa4a81ebe --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py @@ -0,0 +1,58 @@ +"""API for NEW_NAME bound to HASS OAuth.""" +from asyncio import run_coroutine_threadsafe + +from aiohttp import ClientSession +import my_pypi_package + +from homeassistant import core, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +# TODO the following two API examples are based on our suggested best practices +# for libraries using OAuth2 with requests or aiohttp. Delete the one you won't use. +# For more info see the docs at . + + +class ConfigEntryAuth(my_pypi_package.AbstractAuth): + """Provide NEW_NAME authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize NEW_NAME Auth.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(self.session.token) + + def refresh_tokens(self) -> dict: + """Refresh and return new NEW_NAME tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token + + +class AsyncConfigEntryAuth(my_pypi_package.AbstractAuth): + """Provide NEW_NAME authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ): + """Initialize NEW_NAME auth.""" + super().__init__(websession) + self._oauth_session = oauth_session + + async def async_get_access_token(self): + """Return a valid access token.""" + if not self._oauth_session.is_valid: + await self._oauth_session.async_ensure_token_valid() + + return self._oauth_session.token diff --git a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py new file mode 100644 index 00000000000..1112a404e60 --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py @@ -0,0 +1,23 @@ +"""Config flow for NEW_NAME.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle NEW_NAME OAuth2 authentication.""" + + DOMAIN = DOMAIN + # TODO Pick one from config_entries.CONN_CLASS_* + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py new file mode 100644 index 00000000000..7e61bcbfb1b --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -0,0 +1,60 @@ +"""Test the NEW_NAME config flow.""" +from homeassistant import config_entries, setup, data_entry_flow +from homeassistant.components.NEW_DOMAIN.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.helpers import config_entry_oauth2_flow + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "NEW_DOMAIN", + { + "NEW_DOMAIN": { + "type": "oauth2", + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + }, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "NEW_DOMAIN", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "oauth2" diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py index 7ab8b736782..c2ae59aaad4 100644 --- a/script/scaffold/templates/integration/integration/__init__.py +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -1,12 +1,14 @@ """The NEW_NAME integration.""" import voluptuous as vol +from homeassistant.core import HomeAssistant + from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}) +CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict): """Set up the NEW_NAME integration.""" return True From f8efc2adc65b883e764d713f29197a2f97bea8e0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 30 Oct 2019 07:57:40 +0100 Subject: [PATCH 081/306] Fix KeyError in decora setup (#28279) * Imported homeassistant.util and slugified address if no name is specified * Added a custom validator function in case name is not set in config * Removed logger.debug line only used for testing --- homeassistant/components/decora/light.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 4d2d10ccbd5..6ca427f2476 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -1,4 +1,5 @@ """Support for Decora dimmers.""" +import copy from functools import wraps import logging import time @@ -15,17 +16,34 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv +import homeassistant.util as util _LOGGER = logging.getLogger(__name__) SUPPORT_DECORA_LED = SUPPORT_BRIGHTNESS + +def _name_validator(config): + """Validate the name.""" + config = copy.deepcopy(config) + for address, device_config in config[CONF_DEVICES].items(): + if CONF_NAME not in device_config: + device_config[CONF_NAME] = util.slugify(address) + + return config + + DEVICE_SCHEMA = vol.Schema( {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} +PLATFORM_SCHEMA = vol.Schema( + vol.All( + PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} + ), + _name_validator, + ) ) From 4350467a00dc7016814e19328204ed244e0b84bc Mon Sep 17 00:00:00 2001 From: ZiroNL Date: Wed, 30 Oct 2019 08:36:53 +0100 Subject: [PATCH 082/306] Add services.yaml to local_file component. (#28330) --- homeassistant/components/local_file/services.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml index e69de29bb2d..b359b411b6a 100644 --- a/homeassistant/components/local_file/services.yaml +++ b/homeassistant/components/local_file/services.yaml @@ -0,0 +1,9 @@ +local_file_update_file_path: + description: Use this service to change the file displayed by the camera. + fields: + entity_id: + description: Name of the entity_id of the camera to update. + example: 'camera.local_file' + file_path: + description: The full path to the new image file to be displayed. + example: '/config/www/images/image.jpg' From bda3aadbcf9694fa7b25bf9120de92e1d6b25a26 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 30 Oct 2019 09:05:13 -0600 Subject: [PATCH 083/306] Bump pymyq to 2.0.1 (#28348) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 73265b61c83..115c9cf515b 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -3,7 +3,7 @@ "name": "Myq", "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ - "pymyq==2.0.0" + "pymyq==2.0.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 51f7d0816c7..ea7fdfdcdb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1331,7 +1331,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.0 +pymyq==2.0.1 # homeassistant.components.mysensors pymysensors==0.18.0 From 8ae43d2de34412d88a0c6d3f240caa6cf06597f4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Oct 2019 20:49:01 +0100 Subject: [PATCH 084/306] Add device triggers to cover (#28063) * Add device triggers to cover * Use numeric_state trigger instead of template trigger * Tweak translations --- .../components/automation/numeric_state.py | 9 +- .../components/cover/device_trigger.py | 210 ++++++ homeassistant/components/cover/strings.json | 28 +- tests/components/cover/test_device_trigger.py | 702 ++++++++++++++++++ 4 files changed, 935 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/cover/device_trigger.py create mode 100644 tests/components/cover/test_device_trigger.py diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 8d88fe9cae6..0c8ab3d9c8b 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.const import ( CONF_VALUE_TEMPLATE, CONF_PLATFORM, @@ -17,7 +17,8 @@ from homeassistant.helpers.event import async_track_state_change, async_track_sa from homeassistant.helpers import condition, config_validation as cv, template -# mypy: allow-untyped-defs, no-check-untyped-defs +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs TRIGGER_SCHEMA = vol.All( vol.Schema( @@ -42,7 +43,7 @@ _LOGGER = logging.getLogger(__name__) async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="numeric_state" -): +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) @@ -52,7 +53,7 @@ async def async_attach_trigger( value_template = config.get(CONF_VALUE_TEMPLATE) unsub_track_same = {} entities_triggered = set() - period = {} + period: dict = {} if value_template is not None: value_template.hass = hass diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py new file mode 100644 index 00000000000..4f256a87dc5 --- /dev/null +++ b/homeassistant/components/cover/device_trigger.py @@ -0,0 +1,210 @@ +"""Provides device automations for Cover.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + CONF_ABOVE, + CONF_BELOW, + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import ( + state as state_automation, + numeric_state as numeric_state_automation, + AutomationActionType, +) +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import ( + DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, +) + +POSITION_TRIGGER_TYPES = {"position", "tilt_position"} +STATE_TRIGGER_TYPES = {"opened", "closed", "opening", "closing"} + +POSITION_TRIGGER_SCHEMA = vol.All( + TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(POSITION_TRIGGER_TYPES), + vol.Optional(CONF_ABOVE): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(CONF_BELOW): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + +STATE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(STATE_TRIGGER_TYPES), + } +) + +TRIGGER_SCHEMA = vol.Any(POSITION_TRIGGER_SCHEMA, STATE_TRIGGER_SCHEMA) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Cover devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + if not state or ATTR_SUPPORTED_FEATURES not in state.attributes: + continue + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE) + + # Add triggers for each entity that belongs to this integration + if supports_open_close: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "opened", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "closed", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "opening", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "closing", + } + ) + if supported_features & SUPPORT_SET_POSITION: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "position", + } + ) + if supported_features & SUPPORT_SET_TILT_POSITION: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "tilt_position", + } + ) + + return triggers + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + if config[CONF_TYPE] not in ["position", "tilt_position"]: + return {} + + return { + "extra_fields": vol.Schema( + { + vol.Optional(CONF_ABOVE, default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(CONF_BELOW, default=100): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + } + ) + } + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] in STATE_TRIGGER_TYPES: + if config[CONF_TYPE] == "opened": + to_state = STATE_OPEN + elif config[CONF_TYPE] == "closed": + to_state = STATE_CLOSED + elif config[CONF_TYPE] == "opening": + to_state = STATE_OPENING + elif config[CONF_TYPE] == "closing": + to_state = STATE_CLOSING + + state_config = { + state_automation.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_TO: to_state, + } + state_config = state_automation.TRIGGER_SCHEMA(state_config) + return await state_automation.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + if config[CONF_TYPE] == "position": + position = "current_position" + if config[CONF_TYPE] == "tilt_position": + position = "current_tilt_position" + min_pos = config.get(CONF_ABOVE, -1) + max_pos = config.get(CONF_BELOW, 101) + value_template = f"{{{{ state.attributes.{position} }}}}" + + numeric_state_config = { + numeric_state_automation.CONF_PLATFORM: "numeric_state", + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + numeric_state_automation.CONF_BELOW: max_pos, + numeric_state_automation.CONF_ABOVE: min_pos, + numeric_state_automation.CONF_VALUE_TEMPLATE: value_template, + } + numeric_state_config = numeric_state_automation.TRIGGER_SCHEMA(numeric_state_config) + return await numeric_state_automation.async_attach_trigger( + hass, numeric_state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index e4c72746ee4..36492cc5ed5 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -1,12 +1,20 @@ { - "device_automation": { - "condition_type": { - "is_open": "{entity_name} is open", - "is_closed": "{entity_name} is closed", - "is_opening": "{entity_name} is opening", - "is_closing": "{entity_name} is closing", - "is_position": "{entity_name} position is", - "is_tilt_position": "{entity_name} tilt position is" - } + "device_automation": { + "condition_type": { + "is_open": "{entity_name} is open", + "is_closed": "{entity_name} is closed", + "is_opening": "{entity_name} is opening", + "is_closing": "{entity_name} is closing", + "is_position": "Current {entity_name} position is", + "is_tilt_position": "Current {entity_name} tilt position is" + }, + "trigger_type": { + "opened": "{entity_name} opened", + "closed": "{entity_name} closed", + "opening": "{entity_name} opening", + "closing": "{entity_name} closing", + "position": "{entity_name} position changes", + "tilt_position": "{entity_name} tilt position changes" } -} \ No newline at end of file + } +} diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py new file mode 100644 index 00000000000..4f50c0639c0 --- /dev/null +++ b/tests/components/cover/test_device_trigger.py @@ -0,0 +1,702 @@ +"""The tests for Cover device triggers.""" +import pytest + +from homeassistant.components.cover import DOMAIN +from homeassistant.const import ( + CONF_PLATFORM, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[0] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "opened", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_set_pos(hass, device_reg, entity_reg): + """Test we get the expected triggers from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "opened", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "position", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_set_tilt_pos(hass, device_reg, entity_reg): + """Test we get the expected triggers from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "opened", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "tilt_position", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[0] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 4 + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == {"extra_fields": []} + + +async def test_get_trigger_capabilities_set_pos(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "name": "above", + "optional": True, + "type": "integer", + "default": 0, + "valueMax": 100, + "valueMin": 0, + }, + { + "name": "below", + "optional": True, + "type": "integer", + "default": 100, + "valueMax": 100, + "valueMin": 0, + }, + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 5 + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + if trigger["type"] == "position": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + +async def test_get_trigger_capabilities_set_tilt_pos(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "name": "above", + "optional": True, + "type": "integer", + "default": 0, + "valueMax": 100, + "valueMin": 0, + }, + { + "name": "below", + "optional": True, + "type": "integer", + "default": 100, + "valueMax": 100, + "valueMin": 0, + }, + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 5 + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + if trigger["type"] == "tilt_position": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + +async def test_if_fires_on_state_change(hass, calls): + """Test for state triggers firing.""" + hass.states.async_set("cover.entity", STATE_CLOSED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "opened", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "opened - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "closed", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "closed - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "opening", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "opening - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "closing", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "closing - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is opened. + hass.states.async_set("cover.entity", STATE_OPEN) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "opened - device - {} - closed - open - None".format("cover.entity") + + # Fake that the entity is closed. + hass.states.async_set("cover.entity", STATE_CLOSED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data[ + "some" + ] == "closed - device - {} - open - closed - None".format("cover.entity") + + # Fake that the entity is opening. + hass.states.async_set("cover.entity", STATE_OPENING) + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data[ + "some" + ] == "opening - device - {} - closed - opening - None".format("cover.entity") + + # Fake that the entity is closing. + hass.states.async_set("cover.entity", STATE_CLOSING) + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data[ + "some" + ] == "closing - device - {} - opening - closing - None".format("cover.entity") + + +async def test_if_fires_on_position(hass, calls): + """Test for position triggers.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "position", + "above": 45, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_gt_45 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "position", + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_lt_90 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "position", + "above": 45, + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_gt_45_lt_90 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 50} + ) + await hass.async_block_till_done() + assert len(calls) == 3 + assert sorted( + [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] + ) == sorted( + [ + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", + ] + ) + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 95} + ) + await hass.async_block_till_done() + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 45} + ) + await hass.async_block_till_done() + assert len(calls) == 4 + assert ( + calls[3].data["some"] + == "is_pos_lt_90 - device - cover.set_position_cover - closed - closed - None" + ) + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 90} + ) + await hass.async_block_till_done() + assert len(calls) == 5 + assert ( + calls[4].data["some"] + == "is_pos_gt_45 - device - cover.set_position_cover - closed - closed - None" + ) + + +async def test_if_fires_on_tilt_position(hass, calls): + """Test for tilt position triggers.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "tilt_position", + "above": 45, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_gt_45 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "tilt_position", + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_lt_90 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "tilt_position", + "above": 45, + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_gt_45_lt_90 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 50} + ) + await hass.async_block_till_done() + assert len(calls) == 3 + assert sorted( + [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] + ) == sorted( + [ + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", + ] + ) + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 95} + ) + await hass.async_block_till_done() + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 45} + ) + await hass.async_block_till_done() + assert len(calls) == 4 + assert ( + calls[3].data["some"] + == "is_pos_lt_90 - device - cover.set_position_cover - closed - closed - None" + ) + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 90} + ) + await hass.async_block_till_done() + assert len(calls) == 5 + assert ( + calls[4].data["some"] + == "is_pos_gt_45 - device - cover.set_position_cover - closed - closed - None" + ) From ee24710524e0d4b603891f0e187e0fa2fff5b76c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 31 Oct 2019 00:32:14 +0000 Subject: [PATCH 085/306] [ci skip] Translation update --- homeassistant/components/cover/.translations/en.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/.translations/en.json b/homeassistant/components/cover/.translations/en.json index eaac0d557ef..27710f79436 100644 --- a/homeassistant/components/cover/.translations/en.json +++ b/homeassistant/components/cover/.translations/en.json @@ -5,8 +5,16 @@ "is_closing": "{entity_name} is closing", "is_open": "{entity_name} is open", "is_opening": "{entity_name} is opening", - "is_position": "{entity_name} position is", - "is_tilt_position": "{entity_name} tilt position is" + "is_position": "Current {entity_name} position is", + "is_tilt_position": "Current {entity_name} tilt position is" + }, + "trigger_type": { + "closed": "{entity_name} closed", + "closing": "{entity_name} closing", + "opened": "{entity_name} opened", + "opening": "{entity_name} opening", + "position": "{entity_name} position changes", + "tilt_position": "{entity_name} tilt position changes" } } } \ No newline at end of file From d3750401c14920e0444050096e3012e436985094 Mon Sep 17 00:00:00 2001 From: Steve M Date: Thu, 31 Oct 2019 04:38:53 -0400 Subject: [PATCH 086/306] Bump env_canada to fixed 0.0.29 version (#28360) * Bump env_canada to fixed 0.0.29 version * bump env_canada to 0.29 --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index c62e1e356b6..7661269073c 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,7 +3,7 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": [ - "env_canada==0.0.25" + "env_canada==0.0.29" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ea7fdfdcdb5..0966533dc0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -459,7 +459,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.25 +env_canada==0.0.29 # homeassistant.components.envirophat # envirophat==0.0.6 From ef92c5672d020cf4ca03c66fca3ae7d1d7f80a39 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Thu, 31 Oct 2019 09:39:27 +0100 Subject: [PATCH 087/306] Bump pysaj to v0.0.13 (fix for sensor date) (#28351) --- homeassistant/components/saj/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index 2dd701e9c7c..4d02ab74840 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.12" + "pysaj==0.0.13" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0966533dc0b..01c4f879c7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1426,7 +1426,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.12 +pysaj==0.0.13 # homeassistant.components.sony_projector pysdcp==1 From 89df82111314a666811dd24fda704fefc7e27d45 Mon Sep 17 00:00:00 2001 From: Santobert Date: Thu, 31 Oct 2019 09:41:44 +0100 Subject: [PATCH 088/306] Flux log with debug instead of info (#28352) --- homeassistant/components/flux/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 7b58ffbe449..404067d4107 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -310,7 +310,7 @@ class FluxSwitch(SwitchDevice, RestoreEntity): await async_set_lights_xy( self.hass, self._lights, x_val, y_val, brightness, self._transition ) - _LOGGER.info( + _LOGGER.debug( "Lights updated to x:%s y:%s brightness:%s, %s%% " "of %s cycle complete at %s", x_val, @@ -322,7 +322,7 @@ class FluxSwitch(SwitchDevice, RestoreEntity): ) elif self._mode == MODE_RGB: await async_set_lights_rgb(self.hass, self._lights, rgb, self._transition) - _LOGGER.info( + _LOGGER.debug( "Lights updated to rgb:%s, %s%% " "of %s cycle complete at %s", rgb, round(percentage_complete * 100), @@ -335,7 +335,7 @@ class FluxSwitch(SwitchDevice, RestoreEntity): await async_set_lights_temp( self.hass, self._lights, mired, brightness, self._transition ) - _LOGGER.info( + _LOGGER.debug( "Lights updated to mired:%s brightness:%s, %s%% " "of %s cycle complete at %s", mired, From e11c9d710c917d7c2b71e85024a690a91c40ec94 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Thu, 31 Oct 2019 04:49:38 -0400 Subject: [PATCH 089/306] Add modelnumber for ecobee4 (#28278) --- homeassistant/components/ecobee/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 5022cb71903..05c2d22b594 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -20,6 +20,7 @@ ECOBEE_MODEL_TO_NAME = { "nikeSmart": "ecobee3 lite Smart", "nikeEms": "ecobee3 lite EMS", "apolloSmart": "ecobee4 Smart", + "vulcanSmart": "ecobee4 Smart", } ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] From bfe4a85e9d5255fb8dd64b97ae7222f980c83fe0 Mon Sep 17 00:00:00 2001 From: gngj Date: Thu, 31 Oct 2019 10:51:15 +0200 Subject: [PATCH 090/306] Fill services.yaml for duckdns (#28248) * Fill services.yaml for duckdns * Apply suggestions from code review Co-Authored-By: Fabian Affolter --- homeassistant/components/duckdns/services.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/duckdns/services.yaml b/homeassistant/components/duckdns/services.yaml index e69de29bb2d..8c353a0b3cd 100644 --- a/homeassistant/components/duckdns/services.yaml +++ b/homeassistant/components/duckdns/services.yaml @@ -0,0 +1,6 @@ +set_txt: + description: Set the TXT record of your DuckDNS subdomain. + fields: + txt: + description: Payload for the TXT record. + example: 'This domain name is reserved for use in documentation' From ff5b070f4b08abe64411ebb5f4d1713a0f5f59cd Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Thu, 31 Oct 2019 05:38:44 -0400 Subject: [PATCH 091/306] Implement Alexa.SeekController Interface for media_player in Alexa (#28299) * Implement Alexa.SeekController Interface for Alexa * Added error handling and duration checks. * Split out media_player SeekController tests and added error test. --- .../components/alexa/capabilities.py | 11 ++ homeassistant/components/alexa/entities.py | 4 + homeassistant/components/alexa/errors.py | 7 + homeassistant/components/alexa/handlers.py | 43 ++++++ tests/components/alexa/test_smart_home.py | 134 ++++++++++++++++-- 5 files changed, 190 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index b5b7fa88ef8..7d74bb3f8cd 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1175,3 +1175,14 @@ class AlexaPlaybackStateReporter(AlexaCapability): return {"state": "PAUSED"} return {"state": "STOPPED"} + + +class AlexaSeekController(AlexaCapability): + """Implements Alexa.SeekController. + + https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.SeekController" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index ef6c2902053..6d2f9aef56a 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -52,6 +52,7 @@ from .capabilities import ( AlexaRangeController, AlexaSceneController, AlexaSecurityPanelController, + AlexaSeekController, AlexaSpeaker, AlexaStepSpeaker, AlexaTemperatureSensor, @@ -425,6 +426,9 @@ class MediaPlayerCapabilities(AlexaEntity): yield AlexaPlaybackController(self.entity) yield AlexaPlaybackStateReporter(self.entity) + if supported & media_player.const.SUPPORT_SEEK: + yield AlexaSeekController(self.entity) + if supported & media_player.SUPPORT_SELECT_SOURCE: yield AlexaInputController(self.entity) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index b0600313fc2..29643bacc53 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -111,3 +111,10 @@ class AlexaInvalidDirectiveError(AlexaError): namespace = "Alexa" error_type = "INVALID_DIRECTIVE" + + +class AlexaVideoActionNotPermittedForContentError(AlexaError): + """Class to represent action not permitted for content errors.""" + + namespace = "Alexa.Video" + error_type = "ACTION_NOT_PERMITTED_FOR_CONTENT" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 3dadf51509a..c23e01f501f 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -54,6 +54,7 @@ from .errors import ( AlexaSecurityPanelUnauthorizedError, AlexaTempRangeError, AlexaUnsupportedThermostatModeError, + AlexaVideoActionNotPermittedForContentError, ) from .state_report import async_enable_proactive_mode @@ -1186,3 +1187,45 @@ async def async_api_skipchannel(hass, config, directive, context): ) return response + + +@HANDLERS.register(("Alexa.SeekController", "AdjustSeekPosition")) +async def async_api_seek(hass, config, directive, context): + """Process a seek request.""" + entity = directive.entity + position_delta = int(directive.payload["deltaPositionMilliseconds"]) + + current_position = entity.attributes.get(media_player.ATTR_MEDIA_POSITION) + if not current_position: + msg = f"{entity} did not return the current media position." + raise AlexaVideoActionNotPermittedForContentError(msg) + + seek_position = int(current_position) + int(position_delta / 1000) + + if seek_position < 0: + seek_position = 0 + + media_duration = entity.attributes.get(media_player.ATTR_MEDIA_DURATION) + if media_duration and 0 < int(media_duration) < seek_position: + seek_position = media_duration + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_SEEK_POSITION: seek_position, + } + + await hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_MEDIA_SEEK, + data, + blocking=False, + context=context, + ) + + # convert seconds to milliseconds for StateReport. + seek_position = int(seek_position * 1000) + + payload = {"properties": [{"name": "positionMilliseconds", "value": seek_position}]} + return directive.response( + name="StateReport", namespace="Alexa.SeekController", payload=payload + ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3994c3d9f5d..9b901288f26 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -10,6 +10,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -728,14 +729,14 @@ async def test_media_player(hass): capabilities = assert_endpoint_capabilities( appliance, + "Alexa.ChannelController", + "Alexa.EndpointHealth", "Alexa.InputController", + "Alexa.PlaybackController", + "Alexa.PlaybackStateReporter", "Alexa.PowerController", "Alexa.Speaker", "Alexa.StepSpeaker", - "Alexa.PlaybackController", - "Alexa.PlaybackStateReporter", - "Alexa.EndpointHealth", - "Alexa.ChannelController", ) playback_capability = get_capability(capabilities, "Alexa.PlaybackController") @@ -950,14 +951,15 @@ async def test_media_player_power(hass): assert_endpoint_capabilities( appliance, + "Alexa.ChannelController", + "Alexa.EndpointHealth", "Alexa.InputController", - "Alexa.PowerController", - "Alexa.Speaker", - "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.PlaybackStateReporter", - "Alexa.EndpointHealth", - "Alexa.ChannelController", + "Alexa.PowerController", + "Alexa.SeekController", + "Alexa.Speaker", + "Alexa.StepSpeaker", ) await assert_request_calls_service( @@ -996,6 +998,120 @@ async def test_media_player_speaker(hass): assert appliance["friendlyName"] == "Test media player" +async def test_media_player_seek(hass): + """Test media player seek capability.""" + device = ( + "media_player.test_seek", + "playing", + { + "friendly_name": "Test media player seek", + "supported_features": SUPPORT_SEEK, + "media_position": 300, # 5min + "media_duration": 600, # 10min + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test_seek" + assert appliance["displayCategories"][0] == "TV" + assert appliance["friendlyName"] == "Test media player seek" + + assert_endpoint_capabilities( + appliance, + "Alexa.EndpointHealth", + "Alexa.PowerController", + "Alexa.SeekController", + ) + + # Test seek forward 30 seconds. + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": 30000}, + ) + assert call.data["seek_position"] == 330 + assert "properties" in msg["event"]["payload"] + properties = msg["event"]["payload"]["properties"] + assert {"name": "positionMilliseconds", "value": 330000} in properties + + # Test seek reverse 30 seconds. + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": -30000}, + ) + assert call.data["seek_position"] == 270 + assert "properties" in msg["event"]["payload"] + properties = msg["event"]["payload"]["properties"] + assert {"name": "positionMilliseconds", "value": 270000} in properties + + # Test seek backwards more than current position (5 min.) result = 0. + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": -500000}, + ) + assert call.data["seek_position"] == 0 + assert "properties" in msg["event"]["payload"] + properties = msg["event"]["payload"]["properties"] + assert {"name": "positionMilliseconds", "value": 0} in properties + + # Test seek forward more than current duration (10 min.) result = 600 sec. + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": 800000}, + ) + assert call.data["seek_position"] == 600 + assert "properties" in msg["event"]["payload"] + properties = msg["event"]["payload"]["properties"] + assert {"name": "positionMilliseconds", "value": 600000} in properties + + +async def test_media_player_seek_error(hass): + """Test media player seek capability for media_position Error.""" + device = ( + "media_player.test_seek", + "playing", + {"friendly_name": "Test media player seek", "supported_features": SUPPORT_SEEK}, + ) + await discovery_test(device, hass) + + # Test for media_position error. + with pytest.raises(AssertionError): + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": 30000}, + ) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa.Video" + assert msg["payload"]["type"] == "ACTION_NOT_PERMITTED_FOR_CONTENT" + + async def test_alert(hass): """Test alert discovery.""" device = ("alert.test", "off", {"friendly_name": "Test alert"}) From d1335017357ee5109d8b851fb27fd276f71e140b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 31 Oct 2019 17:29:27 +0100 Subject: [PATCH 092/306] Fix Airly asyncio timeout error (#28387) * Raise ConfigEntryNotReady * Better asyncio.TimeoutError handling * Sort imports * Increase asyncio timeout --- homeassistant/components/airly/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index dc2323ddd4e..80f3518c652 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,15 +1,16 @@ """The Airly component.""" import asyncio -import logging from datetime import timedelta +import logging -import async_timeout from aiohttp.client_exceptions import ClientConnectorError from airly import Airly from airly.exceptions import AirlyError +import async_timeout from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle @@ -45,6 +46,9 @@ async def async_setup_entry(hass, config_entry): await airly.async_update() + if not airly.data: + raise ConfigEntryNotReady() + hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly @@ -81,7 +85,7 @@ class AirlyData: """Update Airly data.""" try: - with async_timeout.timeout(10): + with async_timeout.timeout(20): measurements = self.airly.create_measurements_session_point( self.latitude, self.longitude ) @@ -104,11 +108,8 @@ class AirlyData: self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] self.data[ATTR_API_ADVICE] = index["advice"] _LOGGER.debug("Data retrieved from Airly") - except ( - ValueError, - AirlyError, - asyncio.TimeoutError, - ClientConnectorError, - ) as error: + except asyncio.TimeoutError: + _LOGGER.error("Asyncio Timeout Error") + except (ValueError, AirlyError, ClientConnectorError) as error: _LOGGER.error(error) self.data = {} From 89213a4ce83a7f32fd4ee4cd6837b1d0a11c1bdf Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 31 Oct 2019 12:31:06 -0400 Subject: [PATCH 093/306] Don't set entity_id in ZHA entities (#28362) * Don't set entity_id on ZHA entities. * Update tests. * Use comma as separator for multiple channel names. * Address PR comments. --- homeassistant/components/zha/entity.py | 18 +++-------- tests/components/zha/common.py | 35 +++++++++------------ tests/components/zha/test_binary_sensor.py | 13 ++++---- tests/components/zha/test_device_tracker.py | 7 +++-- tests/components/zha/test_fan.py | 7 +++-- tests/components/zha/test_light.py | 15 +++++---- tests/components/zha/test_lock.py | 5 +-- tests/components/zha/test_sensor.py | 14 ++++----- tests/components/zha/test_switch.py | 7 +++-- 9 files changed, 55 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index c11cd405a99..108d8e27a9f 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -9,7 +9,6 @@ from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.util import slugify from .core.const import ( ATTR_MANUFACTURER, @@ -38,17 +37,10 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): self._force_update = False self._should_poll = False self._unique_id = unique_id - if not skip_entity_id: - ieee = zha_device.ieee - ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) - self.entity_id = "{}.{}_{}_{}_{}{}".format( - self._domain, - slugify(zha_device.manufacturer), - slugify(zha_device.model), - ieeetail, - channels[0].cluster.endpoint.endpoint_id, - kwargs.get(ENTITY_SUFFIX, ""), - ) + ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) + ch_names = [ch.cluster.ep_attribute for ch in channels] + ch_names = ", ".join(sorted(ch_names)) + self._name = f"{zha_device.name} {ieeetail} {ch_names}" self._state = None self._device_state_attributes = {} self._zha_device = zha_device @@ -63,7 +55,7 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): @property def name(self): """Return Entity's default name.""" - return self.zha_device.name + return self._name @property def unique_id(self) -> str: diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 788faaaec73..583b4e0738b 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -161,23 +161,22 @@ async def async_setup_entry(hass, config_entry): return True -def make_entity_id(domain, device, cluster, use_suffix=True): - """Make the entity id for the entity under testing. +async def find_entity_id(domain, zha_device, hass): + """Find the entity id under the testing. This is used to get the entity id in order to get the state from the state machine so that we can test state changes. """ - ieee = device.ieee - ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) - entity_id = "{}.{}_{}_{}_{}{}".format( - domain, - slugify(device.manufacturer), - slugify(device.model), - ieeetail, - cluster.endpoint.endpoint_id, - ("", "_{}".format(cluster.cluster_id))[use_suffix], - ) - return entity_id + ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) + head = f"{domain}." + slugify(f"{zha_device.name} {ieeetail}") + + enitiy_ids = hass.states.async_entity_ids(domain) + await hass.async_block_till_done() + + for entity_id in enitiy_ids: + if entity_id.startswith(head): + return entity_id + return None async def async_enable_traffic(hass, zha_gateway, zha_devices): @@ -188,7 +187,7 @@ async def async_enable_traffic(hass, zha_gateway, zha_devices): async def async_test_device_join( - hass, zha_gateway, cluster_id, domain, device_type=None + hass, zha_gateway, cluster_id, entity_id, device_type=None ): """Test a newly joining device. @@ -205,21 +204,15 @@ async def async_test_device_join( "zigpy.zcl.Cluster.bind", return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): - zigpy_device = await async_init_zigpy_device( + await async_init_zigpy_device( hass, [cluster_id, zigpy.zcl.clusters.general.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), is_new_join=True, ) - 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) is not None diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 89dc1ae25a6..2765a465ace 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -11,8 +11,8 @@ from .common import ( async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -27,6 +27,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): [], None, zha_gateway, + ieee="00:0d:6f:11:9a:90:69:e6", ) zigpy_device_occupancy = await async_init_zigpy_device( @@ -46,15 +47,15 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): # on off binary_sensor zone_cluster = zigpy_device_zone.endpoints.get(1).ias_zone - zone_entity_id = make_entity_id(DOMAIN, zigpy_device_zone, zone_cluster) zone_zha_device = zha_gateway.get_device(zigpy_device_zone.ieee) + zone_entity_id = await find_entity_id(DOMAIN, zone_zha_device, hass) + assert zone_entity_id is not None # occupancy binary_sensor occupancy_cluster = zigpy_device_occupancy.endpoints.get(1).occupancy - occupancy_entity_id = make_entity_id( - DOMAIN, zigpy_device_occupancy, occupancy_cluster - ) occupancy_zha_device = zha_gateway.get_device(zigpy_device_occupancy.ieee) + occupancy_entity_id = await find_entity_id(DOMAIN, occupancy_zha_device, hass) + assert occupancy_entity_id is not None # test that the sensors exist and are in the unavailable state assert hass.states.get(zone_entity_id).state == STATE_UNAVAILABLE @@ -76,7 +77,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): # test new sensor join await async_test_device_join( - hass, zha_gateway, measurement.OccupancySensing.cluster_id, DOMAIN + hass, zha_gateway, measurement.OccupancySensing.cluster_id, occupancy_entity_id ) diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 446920eb2f9..bac338ae5e0 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -16,8 +16,8 @@ from .common import ( async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -47,8 +47,9 @@ async def test_device_tracker(hass, config_entry, zha_gateway): await hass.async_block_till_done() cluster = zigpy_device.endpoints.get(1).power - entity_id = make_entity_id(DOMAIN, zigpy_device, cluster, use_suffix=False) zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None # test that the device tracker was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -90,6 +91,6 @@ async def test_device_tracker(hass, config_entry, zha_gateway): hass, zha_gateway, general.PowerConfiguration.cluster_id, - DOMAIN, + entity_id, SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index a196ba50ba7..660bff2abac 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -20,8 +20,8 @@ from .common import ( async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -41,8 +41,9 @@ async def test_fan(hass, config_entry, zha_gateway): await hass.async_block_till_done() cluster = zigpy_device.endpoints.get(1).fan - entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None # test that the fan was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -97,7 +98,7 @@ async def test_fan(hass, config_entry, zha_gateway): assert cluster.write_attributes.call_args == call({"fan_mode": 3}) # test adding new fan to the network and HA - await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, entity_id) async def async_turn_on(hass, entity_id, speed=None): diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index f0d9d4913e6..5180f8f976e 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -14,8 +14,8 @@ from .common import ( async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -35,6 +35,7 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): [], zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_gateway, + ieee="00:0d:6f:11:0a:90:69:e6", ) zigpy_device_level = await async_init_zigpy_device( @@ -58,10 +59,9 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): # on off light on_off_device_on_off_cluster = zigpy_device_on_off.endpoints.get(1).on_off - on_off_entity_id = make_entity_id( - DOMAIN, zigpy_device_on_off, on_off_device_on_off_cluster, use_suffix=False - ) on_off_zha_device = zha_gateway.get_device(zigpy_device_on_off.ieee) + on_off_entity_id = await find_entity_id(DOMAIN, on_off_zha_device, hass) + assert on_off_entity_id is not None # dimmable light level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off @@ -78,10 +78,9 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): ) 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 - ) level_zha_device = zha_gateway.get_device(zigpy_device_level.ieee) + level_entity_id = await find_entity_id(DOMAIN, level_zha_device, hass) + assert level_entity_id is not None # test that the lights were created and that they are unavailable assert hass.states.get(on_off_entity_id).state == STATE_UNAVAILABLE @@ -125,7 +124,7 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): hass, zha_gateway, general.OnOff.cluster_id, - DOMAIN, + on_off_entity_id, device_type=zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, ) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 118526a1d85..1daef317fed 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -11,8 +11,8 @@ from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED from .common import ( async_enable_traffic, async_init_zigpy_device, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -39,8 +39,9 @@ async def test_lock(hass, config_entry, zha_gateway): await hass.async_block_till_done() cluster = zigpy_device.endpoints.get(1).door_lock - entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None # test that the lock was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index dec551f8d62..7746c5d422e 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -12,8 +12,8 @@ from .common import ( async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -85,7 +85,7 @@ async def test_sensor(hass, config_entry, zha_gateway): # test joining a new temperature sensor to the network await async_test_device_join( - hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, DOMAIN + hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, entity_id ) @@ -110,7 +110,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): [], None, zha_gateway, - ieee="{}0:15:8d:00:02:32:4f:32".format(counter), + ieee="00:15:8d:00:02:32:4f:0{}".format(counter), manufacturer="Fake{}".format(cluster_id), model="FakeModel{}".format(cluster_id), ) @@ -126,10 +126,10 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_info = device_infos[cluster_id] zigpy_device = device_info["zigpy_device"] device_info["cluster"] = zigpy_device.endpoints.get(1).in_clusters[cluster_id] - device_info["entity_id"] = make_entity_id( - DOMAIN, zigpy_device, device_info["cluster"] - ) - device_info["zha_device"] = zha_gateway.get_device(zigpy_device.ieee) + zha_device = zha_gateway.get_device(zigpy_device.ieee) + device_info["zha_device"] = zha_device + device_info["entity_id"] = await find_entity_id(DOMAIN, zha_device, hass) + await hass.async_block_till_done() return device_infos diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index bf4ff3ed628..11a0b8f3481 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -11,8 +11,8 @@ from .common import ( async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -39,8 +39,9 @@ async def test_switch(hass, config_entry, zha_gateway): await hass.async_block_till_done() cluster = zigpy_device.endpoints.get(1).on_off - entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None # test that the switch was created and that its state is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -93,4 +94,4 @@ async def test_switch(hass, config_entry, zha_gateway): ) # test joining a new switch to the network and HA - await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, entity_id) From 226b2bc3d0bbdcf5b7de257820e106ce69e2d4f0 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Thu, 31 Oct 2019 10:29:10 -0700 Subject: [PATCH 094/306] Update withings-api to avoid data parsing bugs. (#28382) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index ea9845f3e42..1f2169363e9 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "withings-api==2.1.2" + "withings-api==2.1.3" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 01c4f879c7b..deace09aa1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ websockets==6.0 wirelesstagpy==0.4.0 # homeassistant.components.withings -withings-api==2.1.2 +withings-api==2.1.3 # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8166b9713f4..1e46132c302 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -636,7 +636,7 @@ watchdog==0.8.3 websockets==6.0 # homeassistant.components.withings -withings-api==2.1.2 +withings-api==2.1.3 # homeassistant.components.bluesound # homeassistant.components.startca From 5854eef47b2e0ebfd077fcef2cb3c728881b8c63 Mon Sep 17 00:00:00 2001 From: ZiroNL Date: Thu, 31 Oct 2019 18:57:00 +0100 Subject: [PATCH 095/306] Add services.yaml to onvif component (#28349) --- homeassistant/components/onvif/services.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index e69de29bb2d..667538f056a 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -0,0 +1,16 @@ +onvif_ptz: + description: If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera. + fields: + entity_id: + description: 'String or list of strings that point at entity_ids of cameras. Else targets all.' + example: 'camera.backyard' + tilt: + description: 'Tilt direction. Allowed values: UP, DOWN, NONE' + example: 'UP' + pan: + description: 'Pan direction. Allowed values: RIGHT, LEFT, NONE' + example: 'RIGHT' + zoom: + description: 'Zoom. Allowed values: ZOOM_IN, ZOOM_OUT, NONE' + example: 'NONE' + From 674860e00e18234184bcb7e5d8b0492b1dfe705d Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 31 Oct 2019 20:16:27 +0200 Subject: [PATCH 096/306] Fix hdate spamming homeassistant log (#28392) * Fix hdate spamming homeassistant log * Lower verbosity of another spammy message --- homeassistant/components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 08182daedd0..b343963153d 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -3,7 +3,7 @@ "name": "Jewish calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": [ - "hdate==0.9.1" + "hdate==0.9.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index deace09aa1e..daee288d06c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ hass-nabucasa==0.23 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.1 +hdate==0.9.3 # homeassistant.components.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e46132c302..e757272a789 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ hass-nabucasa==0.23 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.1 +hdate==0.9.3 # homeassistant.components.here_travel_time herepy==0.6.3.1 From 70c4b4a4f05ece724e449a15caf10a39ae5de5b3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Oct 2019 11:38:06 -0700 Subject: [PATCH 097/306] Check for import errors before validating config (#28395) --- homeassistant/setup.py | 17 +++++++++++------ tests/test_setup.py | 8 ++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 07de3b2942d..314938feeed 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -132,6 +132,17 @@ async def _async_setup_component( log_error(str(err)) return False + # Some integrations fail on import because they call functions incorrectly. + # So we do it before validating config to catch these errors. + try: + component = integration.get_component() + except ImportError: + log_error("Unable to import component", False) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Setup failed for %s: unknown error", domain) + return False + processed_config = await conf_util.async_process_component_config( hass, config, integration ) @@ -143,12 +154,6 @@ async def _async_setup_component( start = timer() _LOGGER.info("Setting up %s", domain) - try: - component = integration.get_component() - except ImportError: - log_error("Unable to import component", False) - return False - if hasattr(component, "PLATFORM_SCHEMA"): # Entity components have their own warning warn_task = None diff --git a/tests/test_setup.py b/tests/test_setup.py index 9612c1784ef..8fd25091eb6 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -527,3 +527,11 @@ async def test_when_setup_already_loaded(hass): setup.async_when_setup(hass, "test", mock_callback) await hass.async_block_till_done() assert calls == ["test", "test"] + + +async def test_setup_import_blows_up(hass): + """Test that we handle it correctly when importing integration blows up.""" + with mock.patch( + "homeassistant.loader.Integration.get_component", side_effect=ValueError + ): + assert not await setup.async_setup_component(hass, "sun", {}) From 631a819bd13a5177f03d4c1ae05548961a9968c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Oct 2019 11:39:26 -0700 Subject: [PATCH 098/306] Fix check config (#28393) --- homeassistant/requirements.py | 12 ++++++++++-- tests/test_requirements.py | 21 ++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 95738084f1f..74469ef2fcd 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -35,13 +35,21 @@ async def async_get_integration_with_requirements( This can raise IntegrationNotFound if manifest or integration is invalid, RequirementNotFound if there was some type of failure to install requirements. + + Does not handle circular dependencies. """ integration = await async_get_integration(hass, domain) - if hass.config.skip_pip or not integration.requirements: + if hass.config.skip_pip: return integration - await async_process_requirements(hass, integration.domain, integration.requirements) + if integration.requirements: + await async_process_requirements( + hass, integration.domain, integration.requirements + ) + + for dependency in integration.dependencies: + await async_get_integration_with_requirements(hass, dependency) return integration diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 780b175778e..548ea645360 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -112,7 +112,17 @@ async def test_install_missing_package(hass): async def test_get_integration_with_requirements(hass): """Check getting an integration with loaded requirements.""" hass.config.skip_pip = False - mock_integration(hass, MockModule("test_component", requirements=["hello==1.0.0"])) + mock_integration( + hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) + ) + mock_integration( + hass, + MockModule( + "test_component", + requirements=["test-comp==1.0.0"], + dependencies=["test_component_dep"], + ), + ) with patch( "homeassistant.util.package.is_installed", return_value=False @@ -126,8 +136,13 @@ async def test_get_integration_with_requirements(hass): assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 1 - assert len(mock_inst.mock_calls) == 1 + assert len(mock_is_installed.mock_calls) == 2 + assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" + assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + + assert len(mock_inst.mock_calls) == 2 + assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" + assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" async def test_install_with_wheels_index(hass): From abbf6595bb6a7d34e2bf50e6e6cb369a2901a9f8 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 31 Oct 2019 22:07:07 +0300 Subject: [PATCH 099/306] Fix (#28369) --- homeassistant/components/telegram_bot/manifest.json | 3 ++- requirements_all.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index afc83879cb0..7fb648e5cb5 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -3,7 +3,8 @@ "name": "Telegram bot", "documentation": "https://www.home-assistant.io/integrations/telegram_bot", "requirements": [ - "python-telegram-bot==11.1.0" + "python-telegram-bot==11.1.0", + "PySocks==1.7.1" ], "dependencies": ["http"], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index daee288d06c..135376d5eee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -68,6 +68,9 @@ PyQRCode==1.2.1 # homeassistant.components.rmvtransport PyRMVtransport==0.2.9 +# homeassistant.components.telegram_bot +PySocks==1.7.1 + # homeassistant.components.switchbot # PySwitchbot==0.6.2 From b74711793e76185cee650fdf223a410997ee85a8 Mon Sep 17 00:00:00 2001 From: gngj Date: Thu, 31 Oct 2019 21:32:05 +0200 Subject: [PATCH 100/306] Fill services.yaml for squeezebox (#28247) * fill services.yaml for squeezebox * Minor fix --- homeassistant/components/squeezebox/services.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index e69de29bb2d..05c7de07f42 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -0,0 +1,13 @@ +squeezebox_call_method: + description: Call a custom Squeezebox JSONRPC API. + fields: + entity_id: + description: Name(s) of the Squeezebox entities where to run the API method. + example: 'media_player.squeezebox_radio' + command: + description: Command to pass to Logitech Media Server (p0 in the CLI documentation). + example: 'playlist' + parameters: + description: Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). + example: ["loadtracks", "album.titlesearch="] + From ec373d90c1c64ed11ba061c37c33b62120fcc714 Mon Sep 17 00:00:00 2001 From: thoscut Date: Thu, 31 Oct 2019 20:49:33 +0100 Subject: [PATCH 101/306] Add file list to attributes of folder sensor (#28338) --- homeassistant/components/folder/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index b06d77d93c9..e9e4ea680c4 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -64,10 +64,12 @@ class Folder(Entity): self._size = None self._name = os.path.split(os.path.split(folder_path)[0])[1] self._unit_of_measurement = "MB" + self._file_list = None def update(self): """Update the sensor.""" files_list = get_files_list(self._folder_path, self._filter_term) + self._file_list = files_list self._number_of_files = len(files_list) self._size = get_size(files_list) @@ -96,6 +98,7 @@ class Folder(Entity): "filter": self._filter_term, "number_of_files": self._number_of_files, "bytes": self._size, + "file_list": self._file_list, } return attr From 82729bef70e53eeafeb790bc5b54335c5ce497e7 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 31 Oct 2019 20:51:35 +0100 Subject: [PATCH 102/306] Show all UPNP/IGD sensors in one device (#27517) * show all UPNP/IGD sensors in one device * use device name correctly * Use id of device --- homeassistant/components/upnp/__init__.py | 1 + homeassistant/components/upnp/device.py | 5 +++++ homeassistant/components/upnp/sensor.py | 7 +++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 38356d0af16..bbb49ebd1d4 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -162,6 +162,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): identifiers={(DOMAIN, device.udn)}, name=device.name, manufacturer=device.manufacturer, + model=device.model_name, ) # set up sensors diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 7f7f0f5b93a..de3c93a82ed 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -78,6 +78,11 @@ class Device: """Get the manufacturer.""" return self._igd_device.manufacturer + @property + def model_name(self): + """Get the model name.""" + return self._igd_device.model_name + async def async_add_port_mappings(self, ports, local_ip): """Add port mappings.""" if local_ip == "127.0.0.1": diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 40cb7ef2032..4c85e904b1d 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -93,9 +94,11 @@ class UpnpSensor(Entity): def device_info(self): """Get device info.""" return { - "identifiers": {(DOMAIN_UPNP, self.unique_id)}, - "name": self.name, + "connections": {(dr.CONNECTION_UPNP, self._device.udn)}, + "identifiers": {(DOMAIN_UPNP, self._device.udn)}, + "name": self._device.name, "manufacturer": self._device.manufacturer, + "model": self._device.model_name, } From d200c2dca2f3cf49745b3b3dbf965ab1a165e174 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 1 Nov 2019 07:05:42 +1100 Subject: [PATCH 103/306] fix feedreader handling unrecognized published date (#28225) --- .../components/feedreader/__init__.py | 7 ++++--- tests/components/feedreader/test_init.py | 6 ++++++ tests/fixtures/feedreader4.xml | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/feedreader4.xml diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index e4ec154620e..27b164e4edf 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -139,9 +139,10 @@ class FeedManager: def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" - # We are lucky, `published_parsed` data available, let's make use of - # it to publish only new available entries since the last run - if "published_parsed" in entry.keys(): + # Check if the entry has a published date. + if "published_parsed" in entry.keys() and entry.published_parsed: + # We are lucky, `published_parsed` data available, let's make use of + # it to publish only new available entries since the last run self._has_published_parsed = True self._last_entry_timestamp = max( entry.published_parsed, self._last_entry_timestamp diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 3f1fee0188b..eff44c44303 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -163,6 +163,12 @@ class TestFeedreaderComponent(unittest.TestCase): manager, events = self.setup_manager(feed_data) assert len(events) == 3 + def test_feed_with_unrecognized_publication_date(self): + """Test simple feed with entry with unrecognized publication date.""" + feed_data = load_fixture("feedreader4.xml") + manager, events = self.setup_manager(feed_data) + assert len(events) == 1 + def test_feed_invalid_data(self): """Test feed with invalid data.""" feed_data = "INVALID DATA" diff --git a/tests/fixtures/feedreader4.xml b/tests/fixtures/feedreader4.xml new file mode 100644 index 00000000000..81828ccb6e2 --- /dev/null +++ b/tests/fixtures/feedreader4.xml @@ -0,0 +1,20 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 26 Oct 2019 12:00:00 +1000 + Mon, 26 Oct 2019 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + 26.10.2019 - 12:06:24 + + + + From 54361342ba7368f0a5344c7382815435153ddee9 Mon Sep 17 00:00:00 2001 From: Daniyar Yeralin Date: Thu, 31 Oct 2019 19:15:20 -0400 Subject: [PATCH 104/306] Introduce SUPPORT_COLOR_TEMP for flux_led component (#26692) * Introduce SUPPORT_COLOR_TEMP for flux_led component * Make black linter happy * Remove duplicate SUPPORT_COLOR_TEMP * Make linter happy --- homeassistant/components/flux_led/light.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 5bd84cd157f..7973956848a 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -12,12 +12,14 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE, + ATTR_COLOR_TEMP, EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, + SUPPORT_COLOR_TEMP, Light, PLATFORM_SCHEMA, ) @@ -43,6 +45,10 @@ MODE_RGBW = "rgbw" # RGB value is ignored when this mode is specified. MODE_WHITE = "w" +# Constant color temp values for 2 flux_led special modes +# Warm-white and Cool-white modes +COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF = 285 + # List of supported effects which aren't already declared in LIGHT EFFECT_RED_FADE = "red_fade" EFFECT_GREEN_FADE = "green_fade" @@ -235,7 +241,7 @@ class FluxLight(Light): def supported_features(self): """Flag supported features.""" if self._mode == MODE_RGBW: - return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE + return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE | SUPPORT_COLOR_TEMP if self._mode == MODE_WHITE: return SUPPORT_BRIGHTNESS @@ -284,6 +290,17 @@ class FluxLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) white = kwargs.get(ATTR_WHITE_VALUE) + color_temp = kwargs.get(ATTR_COLOR_TEMP) + + # handle special modes + if color_temp is not None: + if brightness is None: + brightness = self.brightness + if color_temp > COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: + self._bulb.setRgbw(w=brightness) + else: + self._bulb.setRgbw(w2=brightness) + return # Show warning if effect set with rgb, brightness, or white level if effect and (brightness or white or rgb): From bb6a617a6f75ef79d3d8eae6a8ae7f09d6adfea3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 1 Nov 2019 00:32:13 +0000 Subject: [PATCH 105/306] [ci skip] Translation update --- .../components/almond/.translations/pl.json | 9 +++++ .../components/cover/.translations/ca.json | 8 ++++- .../components/cover/.translations/pl.json | 8 +++++ .../components/cover/.translations/ru.json | 9 ++++- .../cover/.translations/zh-Hant.json | 8 +++++ .../device_tracker/.translations/it.json | 8 +++++ .../huawei_lte/.translations/it.json | 32 +++++++++++++++++ .../media_player/.translations/it.json | 9 +++++ .../opentherm_gw/.translations/pl.json | 4 +-- .../components/sensor/.translations/pl.json | 36 +++++++++---------- .../components/withings/.translations/it.json | 6 ++++ 11 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/almond/.translations/pl.json create mode 100644 homeassistant/components/device_tracker/.translations/it.json create mode 100644 homeassistant/components/huawei_lte/.translations/it.json create mode 100644 homeassistant/components/media_player/.translations/it.json diff --git a/homeassistant/components/almond/.translations/pl.json b/homeassistant/components/almond/.translations/pl.json new file mode 100644 index 00000000000..b96d9c09bb2 --- /dev/null +++ b/homeassistant/components/almond/.translations/pl.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Almond.", + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json index 5602ca98ec2..da86e4960c0 100644 --- a/homeassistant/components/cover/.translations/ca.json +++ b/homeassistant/components/cover/.translations/ca.json @@ -2,11 +2,17 @@ "device_automation": { "condition_type": { "is_closed": "{entity_name} est\u00e0 tancat/da", - "is_closing": "{entity_name} est\u00e0 tancan't-se", + "is_closing": "{entity_name} est\u00e0 tancant-se", "is_open": "{entity_name} est\u00e0 obert/a", "is_opening": "{entity_name} s'est\u00e0 obrint", "is_position": "La posici\u00f3 de {entity_name} \u00e9s", "is_tilt_position": "La posici\u00f3 d'inclinaci\u00f3 de {entity_name} \u00e9s" + }, + "trigger_type": { + "closed": "{entity_name} tancat/da", + "closing": "{entity_name} tancant-se", + "opened": "{entity_name} s'ha obert", + "opening": "{entity_name} obrint-se" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json index a4e4f19712b..718c4b86fbd 100644 --- a/homeassistant/components/cover/.translations/pl.json +++ b/homeassistant/components/cover/.translations/pl.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} si\u0119 otwiera", "is_position": "pozycja pokrywy {entity_name} to", "is_tilt_position": "pochylenie pokrywy {entity_name} to" + }, + "trigger_type": { + "closed": "nast\u0105pi zamkni\u0119cie {entity_name}", + "closing": "{entity_name} si\u0119 zamyka", + "opened": "nast\u0105pi otwarcie {entity_name}", + "opening": "{entity_name} si\u0119 otwiera", + "position": "zmieni si\u0119 pozycja pokrywy {entity_name}", + "tilt_position": "zmieni si\u0119 pochylenie pokrywy {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 5e89cc6e82e..47608a1ff77 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -6,7 +6,14 @@ "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "is_position": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438", - "is_tilt_position": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" + "is_tilt_position": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" + }, + "trigger_type": { + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "tilt_position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043d\u0430\u043a\u043b\u043e\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json index 63910cd208c..8197d2146ae 100644 --- a/homeassistant/components/cover/.translations/zh-Hant.json +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f", "is_position": "{entity_name} \u4f4d\u7f6e\u70ba", "is_tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" + }, + "trigger_type": { + "closed": "{entity_name} \u5df2\u95dc\u9589", + "closing": "{entity_name} \u6b63\u5728\u95dc\u9589", + "opened": "{entity_name} \u5df2\u958b\u555f", + "opening": "{entity_name} \u6b63\u5728\u958b\u555f", + "position": "{entity_name} \u4f4d\u7f6e\u8b8a\u66f4", + "tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u8b8a\u66f4" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/it.json b/homeassistant/components/device_tracker/.translations/it.json new file mode 100644 index 00000000000..3030410a97a --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/it.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u00e8 a casa", + "is_not_home": "{entity_name} non \u00e8 a casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json new file mode 100644 index 00000000000..4bfd3389745 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Questo dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "connection_failed": "Connessione fallita", + "incorrect_password": "Password errata", + "incorrect_username": "Nome utente errato", + "incorrect_username_or_password": "Nome utente o password errati", + "invalid_url": "URL non valido" + }, + "step": { + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "Nome utente" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "track_new_devices": "Traccia nuovi dispositivi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/it.json b/homeassistant/components/media_player/.translations/it.json new file mode 100644 index 00000000000..52a9bb3f051 --- /dev/null +++ b/homeassistant/components/media_player/.translations/it.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso", + "is_paused": "{entity_name} \u00e8 in pausa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 180f6a2430d..fe8ccfc8975 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -10,7 +10,7 @@ "init": { "data": { "device": "\u015acie\u017cka lub adres URL", - "floor_temperature": "Temperatura pod\u0142ogi", + "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", "id": "Identyfikator", "name": "Nazwa", "precision": "Precyzja temperatury" @@ -24,7 +24,7 @@ "step": { "init": { "data": { - "floor_temperature": "Temperatura pod\u0142ogi", + "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", "precision": "Precyzja" }, "description": "Opcje dla bramki OpenTherm" diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json index 68a3a0fecfd..7ef982e0d13 100644 --- a/homeassistant/components/sensor/.translations/pl.json +++ b/homeassistant/components/sensor/.translations/pl.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", - "is_humidity": "{entity_name} wilgotno\u015b\u0107", - "is_illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", - "is_power": "moc {entity_name}", - "is_pressure": "ci\u015bnienie {entity_name}", - "is_signal_strength": "si\u0142a sygna\u0142u {entity_name}", - "is_temperature": "temperatura {entity_name}", - "is_timestamp": "znacznik czasu {entity_name}", - "is_value": "warto\u015b\u0107 {entity_name}" + "is_battery_level": "obecny poziom na\u0142adowania baterii {entity_name}", + "is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}", + "is_illuminance": "obecne nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "is_power": "obecna moc {entity_name}", + "is_pressure": "obecne ci\u015bnienie {entity_name}", + "is_signal_strength": "obecna si\u0142a sygna\u0142u {entity_name}", + "is_temperature": "obecna temperatura {entity_name}", + "is_timestamp": "obecny znacznik czasu {entity_name}", + "is_value": "obecna warto\u015b\u0107 {entity_name}" }, "trigger_type": { - "battery_level": "poziom baterii {entity_name}", - "humidity": "wilgotno\u015b\u0107 {entity_name}", - "illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", - "power": "moc {entity_name}", - "pressure": "ci\u015bnienie {entity_name}", - "signal_strength": "si\u0142a sygna\u0142u {entity_name}", - "temperature": "temperatura {entity_name}", - "timestamp": "znacznik czasu {entity_name}", - "value": "warto\u015b\u0107 {entity_name}" + "battery_level": "zmieni si\u0119 poziom baterii {entity_name}", + "humidity": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}", + "illuminance": "zmieni si\u0119 nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "power": "zmieni si\u0119 moc {entity_name}", + "pressure": "zmieni si\u0119 ci\u015bnienie {entity_name}", + "signal_strength": "zmieni si\u0119 si\u0142a sygna\u0142u {entity_name}", + "temperature": "zmieni si\u0119 temperatura {entity_name}", + "timestamp": "zmieni si\u0119 znacznik czasu {entity_name}", + "value": "zmieni si\u0119 warto\u015b\u0107 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index 51276869ec6..fbd590697e1 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -7,6 +7,12 @@ "default": "Autenticazione completata con Withings per il profilo selezionato." }, "step": { + "profile": { + "data": { + "profile": "Profilo" + }, + "title": "Profilo utente." + }, "user": { "data": { "profile": "Profilo" From 72a17d4c677d1a6faa6058a2ba3f99acac62f320 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Nov 2019 13:09:45 +0100 Subject: [PATCH 106/306] Upgrade thingspeak to 1.0.0 (#28424) --- homeassistant/components/thingspeak/__init__.py | 6 ++---- homeassistant/components/thingspeak/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/thingspeak/__init__.py b/homeassistant/components/thingspeak/__init__.py index 1870a317752..0eccfbbcd04 100644 --- a/homeassistant/components/thingspeak/__init__.py +++ b/homeassistant/components/thingspeak/__init__.py @@ -37,20 +37,18 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Thingspeak environment.""" - conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) channel_id = conf.get(CONF_ID) entity = conf.get(CONF_WHITELIST) try: - channel = thingspeak.Channel(channel_id, write_key=api_key, timeout=TIMEOUT) + channel = thingspeak.Channel(channel_id, api_key=api_key, timeout=TIMEOUT) channel.get() except RequestException: _LOGGER.error( "Error while accessing the ThingSpeak channel. " - "Please check that the channel exists and your " - "API key is correct" + "Please check that the channel exists and your API key is correct" ) return False diff --git a/homeassistant/components/thingspeak/manifest.json b/homeassistant/components/thingspeak/manifest.json index 689a6678cab..9fddd941543 100644 --- a/homeassistant/components/thingspeak/manifest.json +++ b/homeassistant/components/thingspeak/manifest.json @@ -3,7 +3,7 @@ "name": "Thingspeak", "documentation": "https://www.home-assistant.io/integrations/thingspeak", "requirements": [ - "thingspeak==0.4.1" + "thingspeak==1.0.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 135376d5eee..1a3da42e9e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1898,7 +1898,7 @@ teslajsonpy==0.0.26 thermoworks_smoke==0.1.8 # homeassistant.components.thingspeak -thingspeak==0.4.1 +thingspeak==1.0.0 # homeassistant.components.tikteck tikteck==0.4 From 083d34cdd94ae0de27eb5ad126c7e44b1c34249e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Nov 2019 13:10:53 +0100 Subject: [PATCH 107/306] Upgrade attrs to 19.3.0 (#28421) --- 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 1933448edda..03e43ba139a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 -attrs==19.2.0 +attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" diff --git a/requirements_all.txt b/requirements_all.txt index 1a3da42e9e8..4d6e59a6912 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 -attrs==19.2.0 +attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" diff --git a/setup.py b/setup.py index e8b32fd8edf..612ac7d0ce8 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ REQUIRES = [ "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", - "attrs==19.2.0", + "attrs==19.3.0", "bcrypt==3.1.7", "certifi>=2019.9.11", 'contextvars==2.4;python_version<"3.7"', From 6f24d2bb3ba345f80925c36452f28b2bc4987d39 Mon Sep 17 00:00:00 2001 From: Michael Schoonmaker Date: Fri, 1 Nov 2019 06:27:26 -0700 Subject: [PATCH 108/306] Add a Services YAML for the Dominos integration (#27289) (#28339) --- homeassistant/components/dominos/services.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/dominos/services.yaml b/homeassistant/components/dominos/services.yaml index e69de29bb2d..93f8b2851f1 100644 --- a/homeassistant/components/dominos/services.yaml +++ b/homeassistant/components/dominos/services.yaml @@ -0,0 +1,6 @@ +order: + description: Places a set of orders with Dominos Pizza. + fields: + order_entity_id: + description: The ID (as specified in the configuration) of an order to place. If provided as an array, all of the identified orders will be placed. + example: dominos.medium_pan From 07337badcdc614ace6ba953c63f0effc1c58ffde Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Nov 2019 14:28:39 +0100 Subject: [PATCH 109/306] Upgrade pysnmp to 4.4.12 (#28428) --- homeassistant/components/snmp/manifest.json | 2 +- homeassistant/components/snmp/sensor.py | 1 - requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index d3942ab4a32..aad7f49c962 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -3,7 +3,7 @@ "name": "Snmp", "documentation": "https://www.home-assistant.io/integrations/snmp", "requirements": [ - "pysnmp==4.4.11" + "pysnmp==4.4.12" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index b369ec83c58..9ca0444f7bc 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -81,7 +81,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SNMP sensor.""" - name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/requirements_all.txt b/requirements_all.txt index 4d6e59a6912..7c12a7580d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1462,7 +1462,7 @@ pysmartthings==0.6.9 pysmarty==0.8 # homeassistant.components.snmp -pysnmp==4.4.11 +pysnmp==4.4.12 # homeassistant.components.soma pysoma==0.0.10 From c7d72f55e9e39c481b0e97946ba9c07f39d37ad6 Mon Sep 17 00:00:00 2001 From: Robin Pronk Date: Fri, 1 Nov 2019 15:38:14 +0100 Subject: [PATCH 110/306] SNMP switch fix integer support (#28425) --- homeassistant/components/snmp/switch.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index aac43208a1f..8d5be1221c4 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,6 +1,8 @@ """Support for SNMP enabled switch.""" import logging +from pyasn1.type.univ import Integer + import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, @@ -190,15 +192,20 @@ class SnmpSwitch(SwitchDevice): async def async_turn_on(self, **kwargs): """Turn on the switch.""" - await self._set(self._command_payload_on) + if self._command_payload_on.isdigit(): + await self._set(Integer(self._command_payload_on)) + else: + await self._set(self._command_payload_on) async def async_turn_off(self, **kwargs): """Turn off the switch.""" - await self._set(self._command_payload_off) + if self._command_payload_on.isdigit(): + await self._set(Integer(self._command_payload_off)) + else: + await self._set(self._command_payload_off) async def async_update(self): """Update the state.""" - errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) ) @@ -215,8 +222,12 @@ class SnmpSwitch(SwitchDevice): for resrow in restable: if resrow[-1] == self._payload_on: self._state = True + elif resrow[-1] == Integer(self._payload_on): + self._state = True elif resrow[-1] == self._payload_off: self._state = False + elif resrow[-1] == Integer(self._payload_off): + self._state = False else: self._state = None From 44879b323e72c667b86e01eebd6c9c50f1f62798 Mon Sep 17 00:00:00 2001 From: John Mihalic <2854333+mezz64@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:40:35 -0400 Subject: [PATCH 111/306] Bump pyEight library to 0.1.2 to update API URL (#28413) --- homeassistant/components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 3353f1fa4d9..25961f15a0a 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -3,7 +3,7 @@ "name": "Eight sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": [ - "pyeight==0.1.1" + "pyeight==0.1.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7c12a7580d5..9723075b98b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,7 +1180,7 @@ pyeconet==0.0.11 pyedimax==0.1 # homeassistant.components.eight_sleep -pyeight==0.1.1 +pyeight==0.1.2 # homeassistant.components.emby pyemby==1.6 From f8d779e84032416c72ea016829670f5f54747d7c Mon Sep 17 00:00:00 2001 From: phispi Date: Fri, 1 Nov 2019 21:23:23 +0100 Subject: [PATCH 112/306] Prevent TypeError when KNX RGB(W) light value contains None (#28358) * Prevent TypeError when KNX RGB(W) light value contains None. * Pylint doesn't like 'w' as variable name, therefore using 'white' instead. * Simplified code as suggested by pvizeli. --- homeassistant/components/knx/light.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 81bf4ad3c83..c7292309461 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -180,13 +180,9 @@ class KNXLight(Light): @property def brightness(self): """Return the brightness of this light between 0..255.""" - if self.device.supports_brightness: - return self.device.current_brightness - if ( - self.device.supports_color or self.device.supports_rgbw - ) and self.device.current_color: - return max(self.device.current_color) - return None + if not self.device.supports_brightness: + return None + return self.device.current_brightness @property def hs_color(self): From 1fb377e61eeea93336d8ca4d7941b3bceb436f61 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 1 Nov 2019 21:25:33 +0100 Subject: [PATCH 113/306] Use defined device class constants for Homematic (#28438) * Use defined device classes for Homematic * Remove not required default None * Missed on None --- .../components/homematic/binary_sensor.py | 32 ++++++++----- homeassistant/components/homematic/sensor.py | 47 ++++++++++++------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 064b6f3e009..cc2907c64fb 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -1,26 +1,32 @@ """Support for HomeMatic binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_SMOKE, + BinarySensorDevice, +) from homeassistant.components.homematic import ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY -from homeassistant.const import DEVICE_CLASS_BATTERY from . import ATTR_DISCOVER_DEVICES, HMDevice _LOGGER = logging.getLogger(__name__) SENSOR_TYPES_CLASS = { - "IPShutterContact": "opening", - "IPShutterContactSabotage": "opening", - "MaxShutterContact": "opening", - "Motion": "motion", - "MotionV2": "motion", - "PresenceIP": "motion", + "IPShutterContact": DEVICE_CLASS_OPENING, + "IPShutterContactSabotage": DEVICE_CLASS_OPENING, + "MaxShutterContact": DEVICE_CLASS_OPENING, + "Motion": DEVICE_CLASS_MOTION, + "MotionV2": DEVICE_CLASS_MOTION, + "PresenceIP": DEVICE_CLASS_PRESENCE, "Remote": None, "RemoteMotion": None, - "ShutterContact": "opening", - "Smoke": "smoke", - "SmokeV2": "smoke", + "ShutterContact": DEVICE_CLASS_OPENING, + "Smoke": DEVICE_CLASS_SMOKE, + "SmokeV2": DEVICE_CLASS_SMOKE, "TiltSensor": None, "WeatherSensor": None, } @@ -56,8 +62,8 @@ class HMBinarySensor(HMDevice, BinarySensorDevice): """Return the class of this sensor from DEVICE_CLASSES.""" # If state is MOTION (Only RemoteMotion working) if self._state == "MOTION": - return "motion" - return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) + return DEVICE_CLASS_MOTION + return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__) def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index a3fc9f7e0fa..10c402a0dd4 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -1,7 +1,15 @@ """Support for HomeMatic sensors.""" import logging -from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT, STATE_UNKNOWN +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_WATT_HOUR, + POWER_WATT, + STATE_UNKNOWN, +) from . import ATTR_DISCOVER_DEVICES, HMDevice @@ -48,21 +56,21 @@ HM_UNIT_HA_CAST = { "VALUE": "#", } -HM_ICON_HA_CAST = { - "WIND_SPEED": "mdi:weather-windy", - "HUMIDITY": "mdi:water-percent", - "TEMPERATURE": "mdi:thermometer", - "ACTUAL_TEMPERATURE": "mdi:thermometer", - "LUX": "mdi:weather-sunny", - "CURRENT_ILLUMINATION": "mdi:weather-sunny", - "AVERAGE_ILLUMINATION": "mdi:weather-sunny", - "LOWEST_ILLUMINATION": "mdi:weather-sunny", - "HIGHEST_ILLUMINATION": "mdi:weather-sunny", - "BRIGHTNESS": "mdi:invert-colors", - "POWER": "mdi:flash-red-eye", - "CURRENT": "mdi:flash-red-eye", +HM_DEVICE_CLASS_HA_CAST = { + "HUMIDITY": DEVICE_CLASS_HUMIDITY, + "TEMPERATURE": DEVICE_CLASS_TEMPERATURE, + "ACTUAL_TEMPERATURE": DEVICE_CLASS_TEMPERATURE, + "LUX": DEVICE_CLASS_ILLUMINANCE, + "CURRENT_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, + "AVERAGE_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, + "LOWEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, + "HIGHEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, + "POWER": DEVICE_CLASS_POWER, + "CURRENT": DEVICE_CLASS_POWER, } +HM_ICON_HA_CAST = {"WIND_SPEED": "mdi:weather-windy", "BRIGHTNESS": "mdi:invert-colors"} + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the HomeMatic sensor platform.""" @@ -86,7 +94,7 @@ class HMSensor(HMDevice): # Does a cast exist for this class? name = self._hmdevice.__class__.__name__ if name in HM_STATE_HA_CAST: - return HM_STATE_HA_CAST[name].get(self._hm_get_state(), None) + return HM_STATE_HA_CAST[name].get(self._hm_get_state()) # No cast, return original value return self._hm_get_state() @@ -94,12 +102,17 @@ class HMSensor(HMDevice): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return HM_UNIT_HA_CAST.get(self._state, None) + return HM_UNIT_HA_CAST.get(self._state) + + @property + def device_class(self): + """Return the device class to use in the frontend, if any.""" + return HM_DEVICE_CLASS_HA_CAST.get(self._state) @property def icon(self): """Return the icon to use in the frontend, if any.""" - return HM_ICON_HA_CAST.get(self._state, None) + return HM_ICON_HA_CAST.get(self._state) def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" From 12f1a8f551af6131ecd58da84630cd94dfa9db85 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 1 Nov 2019 21:36:18 +0100 Subject: [PATCH 114/306] Add improved scene support to the alarm_control_panel integration (#28269) * Add improved scene support to the alarm_control_panel integration * Add service description for alarm_arm_custom_bypass --- .../alarm_control_panel/reproduce_state.py | 84 ++++++++++ .../alarm_control_panel/services.yaml | 10 ++ .../test_reproduce_state.py | 148 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/reproduce_state.py create mode 100644 tests/components/alarm_control_panel/test_reproduce_state.py diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py new file mode 100644 index 00000000000..705bca608a6 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -0,0 +1,84 @@ +"""Reproduce an Alarm control panel state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = { + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ALARM_ARMED_AWAY: + service = SERVICE_ALARM_ARM_AWAY + elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS: + service = SERVICE_ALARM_ARM_CUSTOM_BYPASS + elif state.state == STATE_ALARM_ARMED_HOME: + service = SERVICE_ALARM_ARM_HOME + elif state.state == STATE_ALARM_ARMED_NIGHT: + service = SERVICE_ALARM_ARM_NIGHT + elif state.state == STATE_ALARM_DISARMED: + service = SERVICE_ALARM_DISARM + elif state.state == STATE_ALARM_TRIGGERED: + service = SERVICE_ALARM_TRIGGER + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Alarm control panel states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 7918631464f..9abf2189ed3 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -10,6 +10,16 @@ alarm_disarm: description: An optional code to disarm the alarm control panel with. example: 1234 +alarm_arm_custom_bypass: + description: Send arm custom bypass command. + fields: + entity_id: + description: Name of alarm control panel to arm custom bypass. + example: 'alarm_control_panel.downstairs' + code: + description: An optional code to arm custom bypass the alarm control panel with. + example: 1234 + alarm_arm_home: description: Send the alarm the command for arm home. fields: diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py new file mode 100644 index 00000000000..61b0e3ccd30 --- /dev/null +++ b/tests/components/alarm_control_panel/test_reproduce_state.py @@ -0,0 +1,148 @@ +"""Test reproduce state for Alarm control panel.""" +from homeassistant.const import ( + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Alarm control panel states.""" + hass.states.async_set( + "alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_custom_bypass", + STATE_ALARM_ARMED_CUSTOM_BYPASS, + {}, + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED, {} + ) + + arm_away_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_AWAY + ) + arm_custom_bypass_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_CUSTOM_BYPASS + ) + arm_home_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_HOME + ) + arm_night_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_NIGHT + ) + disarm_calls = async_mock_service(hass, "alarm_control_panel", SERVICE_ALARM_DISARM) + trigger_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_TRIGGER + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY), + State( + "alarm_control_panel.entity_armed_custom_bypass", + STATE_ALARM_ARMED_CUSTOM_BYPASS, + ), + State("alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME), + State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT), + State("alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED), + State("alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED), + ], + blocking=True, + ) + + assert len(arm_away_calls) == 0 + assert len(arm_custom_bypass_calls) == 0 + assert len(arm_home_calls) == 0 + assert len(arm_night_calls) == 0 + assert len(disarm_calls) == 0 + assert len(trigger_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("alarm_control_panel.entity_triggered", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(arm_away_calls) == 0 + assert len(arm_custom_bypass_calls) == 0 + assert len(arm_home_calls) == 0 + assert len(arm_night_calls) == 0 + assert len(disarm_calls) == 0 + assert len(trigger_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("alarm_control_panel.entity_armed_away", STATE_ALARM_TRIGGERED), + State( + "alarm_control_panel.entity_armed_custom_bypass", STATE_ALARM_ARMED_AWAY + ), + State( + "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_CUSTOM_BYPASS + ), + State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_HOME), + State("alarm_control_panel.entity_disarmed", STATE_ALARM_ARMED_NIGHT), + State("alarm_control_panel.entity_triggered", STATE_ALARM_DISARMED), + # Should not raise + State("alarm_control_panel.non_existing", "on"), + ], + blocking=True, + ) + + assert len(arm_away_calls) == 1 + assert arm_away_calls[0].domain == "alarm_control_panel" + assert arm_away_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_custom_bypass" + } + + assert len(arm_custom_bypass_calls) == 1 + assert arm_custom_bypass_calls[0].domain == "alarm_control_panel" + assert arm_custom_bypass_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_home" + } + + assert len(arm_home_calls) == 1 + assert arm_home_calls[0].domain == "alarm_control_panel" + assert arm_home_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_night" + } + + assert len(arm_night_calls) == 1 + assert arm_night_calls[0].domain == "alarm_control_panel" + assert arm_night_calls[0].data == { + "entity_id": "alarm_control_panel.entity_disarmed" + } + + assert len(disarm_calls) == 1 + assert disarm_calls[0].domain == "alarm_control_panel" + assert disarm_calls[0].data == {"entity_id": "alarm_control_panel.entity_triggered"} + + assert len(trigger_calls) == 1 + assert trigger_calls[0].domain == "alarm_control_panel" + assert trigger_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_away" + } From 07b7d514ac8da2d0cc826c652a0527db2566e63f Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 1 Nov 2019 21:37:34 +0100 Subject: [PATCH 115/306] Add improved scene support to the water_heater integration (#28277) --- homeassistant/components/demo/__init__.py | 1 + .../water_heater/reproduce_state.py | 125 ++++++++++++++++++ .../water_heater/test_reproduce_state.py | 124 +++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 homeassistant/components/water_heater/reproduce_state.py create mode 100644 tests/components/water_heater/test_reproduce_state.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 967b7852c6f..d93d217caa7 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -28,6 +28,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ "switch", "tts", "mailbox", + "water_heater", ] diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py new file mode 100644 index 00000000000..2038b4c237b --- /dev/null +++ b/homeassistant/components/water_heater/reproduce_state.py @@ -0,0 +1,125 @@ +"""Reproduce an Water heater state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_AWAY_MODE, + ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, + DOMAIN, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_ELECTRIC, + STATE_GAS, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = { + STATE_ECO, + STATE_ELECTRIC, + STATE_GAS, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_OFF, + STATE_ON, + STATE_PERFORMANCE, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_TEMPERATURE) + == state.attributes.get(ATTR_TEMPERATURE) + and cur_state.attributes.get(ATTR_AWAY_MODE) + == state.attributes.get(ATTR_AWAY_MODE) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state != cur_state.state: + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + else: + service = SERVICE_SET_OPERATION_MODE + service_data[ATTR_OPERATION_MODE] = state.state + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if ( + state.attributes.get(ATTR_TEMPERATURE) + != cur_state.attributes.get(ATTR_TEMPERATURE) + and state.attributes.get(ATTR_TEMPERATURE) is not None + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_TEMPERATURE: state.attributes.get(ATTR_TEMPERATURE), + }, + context=context, + blocking=True, + ) + + if ( + state.attributes.get(ATTR_AWAY_MODE) != cur_state.attributes.get(ATTR_AWAY_MODE) + and state.attributes.get(ATTR_AWAY_MODE) is not None + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AWAY_MODE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_AWAY_MODE: state.attributes.get(ATTR_AWAY_MODE), + }, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Water heater states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/water_heater/test_reproduce_state.py b/tests/components/water_heater/test_reproduce_state.py new file mode 100644 index 00000000000..0c12d8eb54a --- /dev/null +++ b/tests/components/water_heater/test_reproduce_state.py @@ -0,0 +1,124 @@ +"""Test reproduce state for Water heater.""" +from homeassistant.components.water_heater import ( + ATTR_AWAY_MODE, + ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_GAS, +) +from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Water heater states.""" + hass.states.async_set("water_heater.entity_off", STATE_OFF, {}) + hass.states.async_set("water_heater.entity_on", STATE_ON, {ATTR_TEMPERATURE: 45}) + hass.states.async_set("water_heater.entity_away", STATE_ON, {ATTR_AWAY_MODE: True}) + hass.states.async_set("water_heater.entity_gas", STATE_GAS, {}) + hass.states.async_set( + "water_heater.entity_all", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ) + + turn_on_calls = async_mock_service(hass, "water_heater", SERVICE_TURN_ON) + turn_off_calls = async_mock_service(hass, "water_heater", SERVICE_TURN_OFF) + set_op_calls = async_mock_service(hass, "water_heater", SERVICE_SET_OPERATION_MODE) + set_temp_calls = async_mock_service(hass, "water_heater", SERVICE_SET_TEMPERATURE) + set_away_calls = async_mock_service(hass, "water_heater", SERVICE_SET_AWAY_MODE) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("water_heater.entity_off", STATE_OFF), + State("water_heater.entity_on", STATE_ON, {ATTR_TEMPERATURE: 45}), + State("water_heater.entity_away", STATE_ON, {ATTR_AWAY_MODE: True}), + State("water_heater.entity_gas", STATE_GAS, {}), + State( + "water_heater.entity_all", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_op_calls) == 0 + assert len(set_temp_calls) == 0 + assert len(set_away_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("water_heater.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_op_calls) == 0 + assert len(set_temp_calls) == 0 + assert len(set_away_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("water_heater.entity_on", STATE_OFF), + State("water_heater.entity_off", STATE_ON, {ATTR_TEMPERATURE: 45}), + State("water_heater.entity_all", STATE_ECO, {ATTR_AWAY_MODE: False}), + State("water_heater.entity_away", STATE_GAS, {}), + State( + "water_heater.entity_gas", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ), + # Should not raise + State("water_heater.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "water_heater" + assert turn_on_calls[0].data == {"entity_id": "water_heater.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "water_heater" + assert turn_off_calls[0].data == {"entity_id": "water_heater.entity_on"} + + VALID_OP_CALLS = [ + {"entity_id": "water_heater.entity_away", ATTR_OPERATION_MODE: STATE_GAS}, + {"entity_id": "water_heater.entity_gas", ATTR_OPERATION_MODE: STATE_ECO}, + ] + assert len(set_op_calls) == 2 + for call in set_op_calls: + assert call.domain == "water_heater" + assert call.data in VALID_OP_CALLS + VALID_OP_CALLS.remove(call.data) + + VALID_TEMP_CALLS = [ + {"entity_id": "water_heater.entity_off", ATTR_TEMPERATURE: 45}, + {"entity_id": "water_heater.entity_gas", ATTR_TEMPERATURE: 45}, + ] + assert len(set_temp_calls) == 2 + for call in set_temp_calls: + assert call.domain == "water_heater" + assert call.data in VALID_TEMP_CALLS + VALID_TEMP_CALLS.remove(call.data) + + VALID_AWAY_CALLS = [ + {"entity_id": "water_heater.entity_all", ATTR_AWAY_MODE: False}, + {"entity_id": "water_heater.entity_gas", ATTR_AWAY_MODE: True}, + ] + assert len(set_away_calls) == 2 + for call in set_away_calls: + assert call.domain == "water_heater" + assert call.data in VALID_AWAY_CALLS + VALID_AWAY_CALLS.remove(call.data) From 62b09580c4b32812476ba90097c09b34d8bea410 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 22:29:34 +0100 Subject: [PATCH 116/306] deCONZ - Add Xiaomi Aqara Cube device trigger support (#27548) * Add Xiaomi Aqara Cube device trigger support --- .../components/deconz/.translations/en.json | 18 ++++- .../components/deconz/device_trigger.py | 67 +++++++++++++++++++ homeassistant/components/deconz/strings.json | 21 +++++- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index e9c64ffe5fa..6d9f7236d31 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -55,6 +55,12 @@ "left": "Left", "open": "Open", "right": "Right", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6", "turn_off": "Turn off", "turn_on": "Turn on" }, @@ -69,7 +75,17 @@ "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_gyro_activated": "Device shaken" + "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_awakened": "Device awakened", + "remote_falling": "Device in free fall", + "remote_gyro_activated": "Device shaken", + "remote_moved": "Device moved with \"{subtype}\" up", + "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", + "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", + "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", + "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", + "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 2d097d30c0b..919061d9ad8 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -32,7 +32,17 @@ CONF_QUADRUPLE_PRESS = "remote_button_quadruple_press" CONF_QUINTUPLE_PRESS = "remote_button_quintuple_press" CONF_ROTATED = "remote_button_rotated" CONF_ROTATION_STOPPED = "remote_button_rotation_stopped" +CONF_AWAKE = "remote_awakened" +CONF_MOVE = "remote_moved" +CONF_DOUBLE_TAP = "remote_double_tap" CONF_SHAKE = "remote_gyro_activated" +CONF_FREE_FALL = "remote_falling" +CONF_ROTATE_FROM_SIDE_1 = "remote_rotate_from_side_1" +CONF_ROTATE_FROM_SIDE_2 = "remote_rotate_from_side_2" +CONF_ROTATE_FROM_SIDE_3 = "remote_rotate_from_side_3" +CONF_ROTATE_FROM_SIDE_4 = "remote_rotate_from_side_4" +CONF_ROTATE_FROM_SIDE_5 = "remote_rotate_from_side_5" +CONF_ROTATE_FROM_SIDE_6 = "remote_rotate_from_side_6" CONF_TURN_ON = "turn_on" CONF_TURN_OFF = "turn_off" @@ -47,6 +57,13 @@ CONF_BUTTON_1 = "button_1" CONF_BUTTON_2 = "button_2" CONF_BUTTON_3 = "button_3" CONF_BUTTON_4 = "button_4" +CONF_SIDE_1 = "side_1" +CONF_SIDE_2 = "side_2" +CONF_SIDE_3 = "side_3" +CONF_SIDE_4 = "side_4" +CONF_SIDE_5 = "side_5" +CONF_SIDE_6 = "side_6" + HUE_DIMMER_REMOTE_MODEL = "RWL021" HUE_DIMMER_REMOTE = { @@ -129,6 +146,55 @@ TRADFRI_WIRELESS_DIMMER = { (CONF_ROTATED, CONF_RIGHT): 2002, } +AQARA_CUBE_MODEL = "lumi.sensor_cube" +AQARA_CUBE = { + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_2): 6002, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_3): 3002, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_4): 4002, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_5): 1002, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_6): 5002, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_1): 2006, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_3): 3006, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_4): 4006, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_5): 1006, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_6): 5006, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_1): 2003, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_2): 6003, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_4): 4003, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_5): 1003, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_6): 5003, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_1): 2004, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_2): 6004, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_3): 3004, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_5): 1004, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_6): 5004, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_1): 2001, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_2): 6001, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_3): 3001, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_4): 4001, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_6): 5001, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_1): 2005, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_2): 6005, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_3): 3005, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_4): 4005, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_5): 1005, + (CONF_MOVE, CONF_SIDE_1): 2000, + (CONF_MOVE, CONF_SIDE_2): 6000, + (CONF_MOVE, CONF_SIDE_3): 3000, + (CONF_MOVE, CONF_SIDE_4): 4000, + (CONF_MOVE, CONF_SIDE_5): 1000, + (CONF_MOVE, CONF_SIDE_6): 5000, + (CONF_DOUBLE_TAP, CONF_SIDE_1): 2002, + (CONF_DOUBLE_TAP, CONF_SIDE_2): 6002, + (CONF_DOUBLE_TAP, CONF_SIDE_3): 3003, + (CONF_DOUBLE_TAP, CONF_SIDE_4): 4004, + (CONF_DOUBLE_TAP, CONF_SIDE_5): 1001, + (CONF_DOUBLE_TAP, CONF_SIDE_6): 5005, + (CONF_AWAKE, ""): 7000, + (CONF_FREE_FALL, ""): 7008, + (CONF_SHAKE, ""): 7007, +} + AQARA_DOUBLE_WALL_SWITCH_MODEL = "lumi.remote.b286acn01" AQARA_DOUBLE_WALL_SWITCH = { (CONF_SHORT_PRESS, CONF_LEFT): 1002, @@ -179,6 +245,7 @@ REMOTES = { TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, + AQARA_CUBE_MODEL: AQARA_CUBE, AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 3571a9e1207..56186feb8b1 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -65,7 +65,17 @@ "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", - "remote_gyro_activated": "Device shaken" + "remote_falling": "Device in free fall", + "remote_awakened": "Device awakened", + "remote_moved": "Device moved with \"{subtype}\" up", + "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_gyro_activated": "Device shaken", + "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", + "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", + "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", + "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", + "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" }, "trigger_subtype": { "turn_on": "Turn on", @@ -80,7 +90,12 @@ "button_1": "First button", "button_2": "Second button", "button_3": "Third button", - "button_4": "Fourth button" - } + "button_4": "Fourth button", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6" } } From 557e585e561f1c907e84ab876db0c8d287f1cf73 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 22:31:22 +0100 Subject: [PATCH 117/306] deCONZ - Support creating battery sensor when reported (#27538) --- .../components/deconz/binary_sensor.py | 4 +- homeassistant/components/deconz/climate.py | 4 +- homeassistant/components/deconz/sensor.py | 67 +++++++++++++++++-- tests/components/deconz/test_sensor.py | 23 +++++++ 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index b81ecdc5164..1a4d9680c1e 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -26,13 +26,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_handler = DeconzEntityHandler(gateway) @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors, new=True): """Add binary sensor from deCONZ.""" entities = [] for sensor in sensors: - if sensor.BINARY: + if new and sensor.BINARY: new_sensor = DeconzBinarySensor(sensor, gateway) entity_handler.add_entity(new_sensor) entities.append(new_sensor) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index b7a1ebce22a..ba1f1ce846a 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -31,13 +31,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) @callback - def async_add_climate(sensors): + def async_add_climate(sensors, new=True): """Add climate devices from deCONZ.""" entities = [] for sensor in sensors: - if sensor.type in Thermostat.ZHATYPE: + if new and sensor.type in Thermostat.ZHATYPE: entities.append(DeconzThermostat(sensor, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index cc3f3de3170..3a3dbceb46b 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -3,7 +3,10 @@ from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch, Th from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice @@ -25,21 +28,23 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) batteries = set() + battery_handler = DeconzBatteryHandler(gateway) entity_handler = DeconzEntityHandler(gateway) @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors, new=True): """Add sensors from deCONZ. Create DeconzEvent if part of ZHAType list. Create DeconzSensor if not a ZHAType and not a binary sensor. Create DeconzBattery if sensor has a battery attribute. + If new is false it means an existing sensor has got a battery state reported. """ entities = [] for sensor in sensors: - if sensor.type in Switch.ZHATYPE: + if new and sensor.type in Switch.ZHATYPE: if gateway.option_allow_clip_sensor or not sensor.type.startswith( "CLIP" @@ -48,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - elif not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: + elif new and not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: new_sensor = DeconzSensor(sensor, gateway) entity_handler.add_entity(new_sensor) @@ -59,6 +64,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if new_battery.unique_id not in batteries: batteries.add(new_battery.unique_id) entities.append(new_battery) + battery_handler.remove_tracker(sensor) + else: + battery_handler.create_tracker(sensor) async_add_entities(entities, True) @@ -176,3 +184,54 @@ class DeconzBattery(DeconzDevice): attr[ATTR_EVENT_ID] = event.event_id return attr + + +class DeconzSensorStateTracker: + """Track sensors without a battery state and signal when battery state exist.""" + + def __init__(self, sensor, gateway): + """Set up tracker.""" + self.sensor = sensor + self.gateway = gateway + sensor.register_async_callback(self.async_update_callback) + + @callback + def close(self): + """Clean up tracker.""" + self.sensor.remove_callback(self.async_update_callback) + self.gateway = None + self.sensor = None + + @callback + def async_update_callback(self): + """Sensor state updated.""" + if "battery" in self.sensor.changed_keys: + async_dispatcher_send( + self.gateway.hass, + self.gateway.async_signal_new_device(NEW_SENSOR), + [self.sensor], + False, + ) + + +class DeconzBatteryHandler: + """Creates and stores trackers for sensors without a battery state.""" + + def __init__(self, gateway): + """Set up battery handler.""" + self.gateway = gateway + self._trackers = set() + + @callback + def create_tracker(self, sensor): + """Create new tracker for battery state.""" + self._trackers.add(DeconzSensorStateTracker(sensor, self.gateway)) + + @callback + def remove_tracker(self, sensor): + """Remove tracker of battery state.""" + for tracker in self._trackers: + if sensor == tracker.sensor: + tracker.close() + self._trackers.remove(tracker) + break diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 928e527dd07..7b6ae41086b 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -233,3 +233,26 @@ async def test_add_new_sensor(hass): light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" + + +async def test_add_battery_later(hass): + """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = {"1": deepcopy(SENSORS["3"])} + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + remote = gateway.api.sensors["1"] + assert len(gateway.deconz_ids) == 0 + assert len(gateway.events) == 1 + assert len(remote._async_callbacks) == 2 + + remote.async_update({"config": {"battery": 50}}) + await hass.async_block_till_done() + + assert len(gateway.deconz_ids) == 1 + assert len(gateway.events) == 1 + assert len(remote._async_callbacks) == 2 + + battery_sensor = hass.states.get("sensor.switch_1_battery_level") + assert battery_sensor is not None From 21d48218aa8a73749f0d051ac587887bebf23be9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 1 Nov 2019 16:41:26 -0500 Subject: [PATCH 118/306] Use server-specific unique_ids for Plex media_players (#28447) --- homeassistant/components/plex/media_player.py | 27 ++++++++++++++++--- homeassistant/components/plex/server.py | 3 ++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 32bf7b65fff..4c32c1e6376 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -6,7 +6,7 @@ from xml.etree.ElementTree import ParseError import plexapi.exceptions import requests.exceptions -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -30,6 +30,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util import dt as dt_util from .const import ( @@ -56,10 +57,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex media_player from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] + registry = await async_get_registry(hass) def async_new_media_players(new_entities): _async_add_entities( - hass, config_entry, async_add_entities, server_id, new_entities + hass, registry, config_entry, async_add_entities, server_id, new_entities ) unsub = async_dispatcher_connect( @@ -70,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def _async_add_entities( - hass, config_entry, async_add_entities, server_id, new_entities + hass, registry, config_entry, async_add_entities, server_id, new_entities ): """Set up Plex media_player entities.""" entities = [] @@ -79,6 +81,19 @@ def _async_add_entities( plex_mp = PlexMediaPlayer(plexserver, **entity_params) entities.append(plex_mp) + # Migration to per-server unique_ids + old_entity_id = registry.async_get_entity_id( + MP_DOMAIN, PLEX_DOMAIN, plex_mp.machine_identifier + ) + if old_entity_id is not None: + new_unique_id = f"{server_id}:{plex_mp.machine_identifier}" + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + plex_mp.machine_identifier, + new_unique_id, + ) + registry.async_update_entity(old_entity_id, new_unique_id=new_unique_id) + async_add_entities(entities, True) @@ -126,6 +141,7 @@ class PlexMediaPlayer(MediaPlayerDevice): async def async_added_to_hass(self): """Run when about to be added to hass.""" server_id = self.plex_server.machine_identifier + unsub = async_dispatcher_connect( self.hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), @@ -315,6 +331,11 @@ class PlexMediaPlayer(MediaPlayerDevice): @property def unique_id(self): """Return the id of this plex client.""" + return f"{self.plex_server.machine_identifier}:{self._machine_identifier}" + + @property + def machine_identifier(self): + """Return the Plex-provided identifier of this plex client.""" return self._machine_identifier @property diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index e6f77a310f1..28380e714ac 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -94,9 +94,10 @@ class PlexServer: def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" + unique_id = f"{self.machine_identifier}:{machine_identifier}" dispatcher_send( self._hass, - PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier), + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), device, session, ) From 6cc947abbfd771cbc480916bf6bf88f1c0ab673e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 23:06:29 +0100 Subject: [PATCH 119/306] deCONZ - Add Hue dimmer gen1 (rwl020) support to device triggers(#28450) --- homeassistant/components/deconz/device_trigger.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 919061d9ad8..30fbfdd05ae 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -65,7 +65,8 @@ CONF_SIDE_5 = "side_5" CONF_SIDE_6 = "side_6" -HUE_DIMMER_REMOTE_MODEL = "RWL021" +HUE_DIMMER_REMOTE_MODEL_GEN1 = "RWL020" +HUE_DIMMER_REMOTE_MODEL_GEN2 = "RWL021" HUE_DIMMER_REMOTE = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1000, (CONF_SHORT_RELEASE, CONF_TURN_ON): 1002, @@ -238,7 +239,8 @@ AQARA_SQUARE_SWITCH = { } REMOTES = { - HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, + HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, + HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, From c0e1b97119ff2f128c73cfc7e77357a3b0fc51a4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 23:36:23 +0100 Subject: [PATCH 120/306] deCONZ - Improve discovery logging (#28452) --- homeassistant/components/deconz/config_flow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 5ede8e715b9..b9a299230ad 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import ( + _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, @@ -176,6 +177,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): uuid = discovery_info[ATTR_UUID].replace("uuid:", "") + _LOGGER.debug("deCONZ gateway discovered (%s)", uuid) + for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): return await self._update_entry(entry, discovery_info[CONF_HOST]) From 6655b7db2c158297bcc2f8e5a1f708188ab6bd2a Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 1 Nov 2019 23:53:42 +0100 Subject: [PATCH 121/306] Add scene.create service (#28300) * Initial commit * Fix scene.create --- .../components/homeassistant/scene.py | 21 ++++++++++ homeassistant/components/scene/services.yaml | 14 +++++++ tests/components/homeassistant/test_scene.py | 39 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 39b04f6d3ea..45560d30edb 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -54,6 +54,8 @@ def _convert_states(states): return result +CONF_SCENE_ID = "scene_id" + STATES_SCHEMA = vol.All(dict, _convert_states) PLATFORM_SCHEMA = vol.Schema( @@ -72,7 +74,12 @@ PLATFORM_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +CREATE_SCENE_SCHEMA = vol.Schema( + {vol.Required(CONF_SCENE_ID): cv.slug, vol.Required(CONF_ENTITIES): STATES_SCHEMA} +) + SERVICE_APPLY = "apply" +SERVICE_CREATE = "create" SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) _LOGGER = logging.getLogger(__name__) @@ -129,6 +136,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= vol.Schema({vol.Required(CONF_ENTITIES): STATES_SCHEMA}), ) + async def create_service(call): + """Create a scene.""" + scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], call.data[CONF_ENTITIES]) + entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" + if hass.states.get(entity_id) is not None: + _LOGGER.warning("The scene %s already exists", entity_id) + return + + async_add_entities([HomeAssistantScene(hass, scene_config)]) + + hass.services.async_register( + SCENE_DOMAIN, SERVICE_CREATE, create_service, CREATE_SCENE_SCHEMA + ) + def _process_scenes_config(hass, async_add_entities, config): """Process multiple scenes and add them.""" diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 0f1e7103aaf..9cf1b9010a8 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -20,3 +20,17 @@ apply: light.ceiling: state: "on" brightness: 80 + +create: + description: Creates a new scene. + fields: + scene_id: + description: The entity_id of the new scene. + example: all_lights + entities: + description: The entities to control with the scene. + example: + light.tv_back_light: "on" + light.ceiling: + state: "on" + brightness: 200 diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index c7c3f2bc5d5..08e40e23d12 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -51,3 +51,42 @@ async def test_apply_service(hass): state = hass.states.get("light.bed_light") assert state.state == "on" assert state.attributes["brightness"] == 50 + + +async def test_create_service(hass, caplog): + """Test the create service.""" + assert await async_setup_component(hass, "scene", {}) + assert hass.states.get("scene.hallo") is None + + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + }, + blocking=True, + ) + + await hass.async_block_till_done() + scene = hass.states.get("scene.hallo") + assert scene is not None + assert scene.domain == "scene" + assert scene.name == "hallo" + assert scene.state == "scening" + assert scene.attributes.get("entity_id") == ["light.bed_light"] + + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + }, + blocking=True, + ) + + await hass.async_block_till_done() + assert "The scene scene.hallo already exists" in caplog.text + assert hass.states.get("scene.hallo") is not None + assert hass.states.get("scene.hallo_2") is None From 50affdf9530630b259f0d95ee7332f92d58d7a0d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Nov 2019 17:21:50 -0700 Subject: [PATCH 122/306] Also install after_deps (#28453) --- homeassistant/requirements.py | 8 ++++++-- tests/test_requirements.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 74469ef2fcd..a0eec0f442b 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -48,8 +48,12 @@ async def async_get_integration_with_requirements( hass, integration.domain, integration.requirements ) - for dependency in integration.dependencies: - await async_get_integration_with_requirements(hass, dependency) + deps = integration.dependencies + (integration.after_dependencies or []) + + if deps: + await asyncio.gather( + *[async_get_integration_with_requirements(hass, dep) for dep in deps] + ) return integration diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 548ea645360..782b4386552 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -115,12 +115,19 @@ async def test_get_integration_with_requirements(hass): mock_integration( hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) ) + mock_integration( + hass, + MockModule( + "test_component_after_dep", requirements=["test-comp-after-dep==1.0.0"] + ), + ) mock_integration( hass, MockModule( "test_component", requirements=["test-comp==1.0.0"], dependencies=["test_component_dep"], + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, ), ) @@ -136,13 +143,15 @@ async def test_get_integration_with_requirements(hass): assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 2 + assert len(mock_is_installed.mock_calls) == 3 assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + assert mock_is_installed.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" - assert len(mock_inst.mock_calls) == 2 + assert len(mock_inst.mock_calls) == 3 assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + assert mock_inst.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" async def test_install_with_wheels_index(hass): From ad4a960ed2296e4a58713e1d3cfba2da27a3e6ed Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Fri, 1 Nov 2019 17:28:50 -0700 Subject: [PATCH 123/306] Change Abode cache file path, add cache path to config flow (#28389) * Changed cache file path * Cache file naming scheme matches original * Restart tests * Adding cache path to config_flow.py * Moved DEFAULT_CACHEDB to consts file * Use correct cache path * Linting issues --- homeassistant/components/abode/__init__.py | 4 +--- homeassistant/components/abode/config_flow.py | 7 +++++-- homeassistant/components/abode/const.py | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 6a72ac64145..76c14d7917f 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -23,14 +23,12 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -from .const import ATTRIBUTION, DOMAIN +from .const import ATTRIBUTION, DOMAIN, DEFAULT_CACHEDB _LOGGER = logging.getLogger(__name__) CONF_POLLING = "polling" -DEFAULT_CACHEDB = "./abodepy_cache.pickle" - SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_TRIGGER = "trigger_quick_action" diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index d8d914f7998..bf48e4546b3 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import DOMAIN # pylint: disable=W0611 +from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=W0611 CONF_POLLING = "polling" @@ -42,9 +42,12 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] polling = user_input.get(CONF_POLLING, False) + cache = self.hass.config.path(DEFAULT_CACHEDB) try: - await self.hass.async_add_executor_job(Abode, username, password, True) + await self.hass.async_add_executor_job( + Abode, username, password, True, True, True, cache + ) except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py index 35e74e154cf..092843ba212 100644 --- a/homeassistant/components/abode/const.py +++ b/homeassistant/components/abode/const.py @@ -1,3 +1,5 @@ """Constants for the Abode Security System component.""" DOMAIN = "abode" ATTRIBUTION = "Data provided by goabode.com" + +DEFAULT_CACHEDB = "abodepy_cache.pickle" From 15900094a1615b877b2c755098db73b596f9455b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Nov 2019 01:30:37 +0100 Subject: [PATCH 124/306] Update MQTT sensor test (#28449) --- tests/components/mqtt/test_sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index f46d8ab7f71..cd55a08482d 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -268,12 +268,12 @@ async def test_setting_sensor_attribute_via_legacy_mqtt_json_message(hass, mqtt_ "name": "test", "state_topic": "test-topic", "unit_of_measurement": "fav unit", - "json_attributes": "val", + "json_attributes_topic": "test-attributes-topic", } }, ) - async_fire_mqtt_message(hass, "test-topic", '{ "val": "100" }') + async_fire_mqtt_message(hass, "test-attributes-topic", '{ "val": "100" }') state = hass.states.get("sensor.test") assert state.attributes.get("val") == "100" @@ -290,12 +290,12 @@ async def test_update_with_legacy_json_attrs_not_dict(hass, mqtt_mock, caplog): "name": "test", "state_topic": "test-topic", "unit_of_measurement": "fav unit", - "json_attributes": "val", + "json_attributes_topic": "test-attributes-topic", } }, ) - async_fire_mqtt_message(hass, "test-topic", '[ "list", "of", "things"]') + async_fire_mqtt_message(hass, "test-attributes-topic", '[ "list", "of", "things"]') state = hass.states.get("sensor.test") assert state.attributes.get("val") is None @@ -313,12 +313,12 @@ async def test_update_with_legacy_json_attrs_bad_JSON(hass, mqtt_mock, caplog): "name": "test", "state_topic": "test-topic", "unit_of_measurement": "fav unit", - "json_attributes": "val", + "json_attributes_topic": "test-attributes-topic", } }, ) - async_fire_mqtt_message(hass, "test-topic", "This is not JSON") + async_fire_mqtt_message(hass, "test-attributes-topic", "This is not JSON") state = hass.states.get("sensor.test") assert state.attributes.get("val") is None From 4863face6908fb4a942695aaa17feb58bad823e5 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 2 Nov 2019 00:31:48 +0000 Subject: [PATCH 125/306] [ci skip] Translation update --- .../components/abode/.translations/bg.json | 22 ++++++++++ .../components/adguard/.translations/bg.json | 2 + .../components/airly/.translations/bg.json | 22 ++++++++++ .../alarm_control_panel/.translations/bg.json | 11 +++++ .../components/almond/.translations/bg.json | 9 ++++ .../components/almond/.translations/de.json | 9 ++++ .../components/almond/.translations/it.json | 9 ++++ .../components/almond/.translations/ko.json | 9 ++++ .../components/almond/.translations/no.json | 9 ++++ .../components/axis/.translations/bg.json | 1 + .../binary_sensor/.translations/bg.json | 2 + .../cert_expiry/.translations/bg.json | 4 +- .../coolmaster/.translations/bg.json | 23 ++++++++++ .../components/cover/.translations/bg.json | 20 +++++++++ .../components/cover/.translations/ca.json | 4 +- .../components/cover/.translations/de.json | 6 +++ .../components/cover/.translations/it.json | 12 +++++- .../components/cover/.translations/ko.json | 8 ++++ .../components/cover/.translations/no.json | 8 ++++ .../cover/.translations/zh-Hant.json | 4 +- .../components/deconz/.translations/bg.json | 2 + .../components/deconz/.translations/en.json | 18 +------- .../device_tracker/.translations/bg.json | 8 ++++ .../device_tracker/.translations/it.json | 4 +- .../components/glances/.translations/bg.json | 37 ++++++++++++++++ .../huawei_lte/.translations/bg.json | 39 +++++++++++++++++ .../huawei_lte/.translations/it.json | 13 ++++-- .../components/lock/.translations/bg.json | 13 ++++++ .../media_player/.translations/bg.json | 11 +++++ .../media_player/.translations/it.json | 4 +- .../components/neato/.translations/bg.json | 27 ++++++++++++ .../opentherm_gw/.translations/bg.json | 34 +++++++++++++++ .../components/plex/.translations/bg.json | 17 ++++++++ .../components/sensor/.translations/bg.json | 26 +++++++++++ .../components/solarlog/.translations/bg.json | 21 +++++++++ .../components/soma/.translations/bg.json | 23 ++++++++++ .../components/somfy/.translations/bg.json | 5 +++ .../components/somfy/.translations/it.json | 5 +++ .../transmission/.translations/bg.json | 43 +++++++++++++++++++ .../components/unifi/.translations/bg.json | 5 +++ .../components/withings/.translations/bg.json | 7 +++ .../components/withings/.translations/it.json | 1 + .../components/zha/.translations/bg.json | 4 ++ 43 files changed, 533 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/abode/.translations/bg.json create mode 100644 homeassistant/components/airly/.translations/bg.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/bg.json create mode 100644 homeassistant/components/almond/.translations/bg.json create mode 100644 homeassistant/components/almond/.translations/de.json create mode 100644 homeassistant/components/almond/.translations/it.json create mode 100644 homeassistant/components/almond/.translations/ko.json create mode 100644 homeassistant/components/almond/.translations/no.json create mode 100644 homeassistant/components/coolmaster/.translations/bg.json create mode 100644 homeassistant/components/cover/.translations/bg.json create mode 100644 homeassistant/components/device_tracker/.translations/bg.json create mode 100644 homeassistant/components/glances/.translations/bg.json create mode 100644 homeassistant/components/huawei_lte/.translations/bg.json create mode 100644 homeassistant/components/lock/.translations/bg.json create mode 100644 homeassistant/components/media_player/.translations/bg.json create mode 100644 homeassistant/components/neato/.translations/bg.json create mode 100644 homeassistant/components/opentherm_gw/.translations/bg.json create mode 100644 homeassistant/components/sensor/.translations/bg.json create mode 100644 homeassistant/components/solarlog/.translations/bg.json create mode 100644 homeassistant/components/soma/.translations/bg.json create mode 100644 homeassistant/components/transmission/.translations/bg.json diff --git a/homeassistant/components/abode/.translations/bg.json b/homeassistant/components/abode/.translations/bg.json new file mode 100644 index 00000000000..29e3f342cf4 --- /dev/null +++ b/homeassistant/components/abode/.translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Abode.", + "identifier_exists": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d.", + "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0412\u0430\u0448\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0432\u0445\u043e\u0434 \u0432 Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/bg.json b/homeassistant/components/adguard/.translations/bg.json index 826244544b5..398927d370a 100644 --- a/homeassistant/components/adguard/.translations/bg.json +++ b/homeassistant/components/adguard/.translations/bg.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 AdGuard Home {minimal_version} \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 {minimal_version}, \u0438\u043c\u0430\u0442\u0435 {current_version}. \u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u0437\u0430 Hass.io AdGuard Home.", + "adguard_home_outdated": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 AdGuard Home {minimal_version} \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 {minimal_version}, \u0438\u043c\u0430\u0442\u0435 {current_version}.", "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home." }, diff --git a/homeassistant/components/airly/.translations/bg.json b/homeassistant/components/airly/.translations/bg.json new file mode 100644 index 00000000000..c91190d9852 --- /dev/null +++ b/homeassistant/components/airly/.translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \u043a\u043b\u044e\u0447\u044a\u0442 \u043d\u0435 \u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d.", + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430.", + "wrong_location": "\u0412 \u0442\u0430\u0437\u0438 \u043e\u0431\u043b\u0430\u0441\u0442 \u043d\u044f\u043c\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u0442\u0435\u043b\u043d\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447 \u0437\u0430 Airly", + "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0430 \u0432\u044a\u0437\u0434\u0443\u0445\u0430 Airly \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 \u043a\u043b\u044e\u0447 \u0437\u0430 API, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/bg.json b/homeassistant/components/alarm_control_panel/.translations/bg.json new file mode 100644 index 00000000000..29700793770 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/bg.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u0441\u044a\u0441\u0442\u0432\u0438\u0435", + "arm_home": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c \u0432\u043a\u044a\u0449\u0438", + "arm_night": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u043d\u043e\u0449\u0435\u043d \u0440\u0435\u0436\u0438\u043c", + "disarm": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439 {entity_name}", + "trigger": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0435 {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/bg.json b/homeassistant/components/almond/.translations/bg.json new file mode 100644 index 00000000000..da5571ad029 --- /dev/null +++ b/homeassistant/components/almond/.translations/bg.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Almond \u0430\u043a\u0430\u0443\u043d\u0442.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/de.json b/homeassistant/components/almond/.translations/de.json new file mode 100644 index 00000000000..4e2816cb001 --- /dev/null +++ b/homeassistant/components/almond/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Sie k\u00f6nnen nur ein Almond-Konto konfigurieren.", + "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json new file mode 100644 index 00000000000..a7e207e899b --- /dev/null +++ b/homeassistant/components/almond/.translations/it.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Almond.", + "cannot_connect": "Impossibile connettersi al server Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ko.json b/homeassistant/components/almond/.translations/ko.json new file mode 100644 index 00000000000..9440242ebbc --- /dev/null +++ b/homeassistant/components/almond/.translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/no.json b/homeassistant/components/almond/.translations/no.json new file mode 100644 index 00000000000..37888debe78 --- /dev/null +++ b/homeassistant/components/almond/.translations/no.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en Almond konto.", + "cannot_connect": "Kan ikke koble til Almond-serveren." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/bg.json b/homeassistant/components/axis/.translations/bg.json index b8c5bf7609f..c56822ba5a4 100644 --- a/homeassistant/components/axis/.translations/bg.json +++ b/homeassistant/components/axis/.translations/bg.json @@ -12,6 +12,7 @@ "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0447\u043d\u043e", "faulty_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" }, + "flow_title": "Axis \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/bg.json b/homeassistant/components/binary_sensor/.translations/bg.json index 9b9741b9601..373866ecd8c 100644 --- a/homeassistant/components/binary_sensor/.translations/bg.json +++ b/homeassistant/components/binary_sensor/.translations/bg.json @@ -53,6 +53,7 @@ "hot": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", "light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "moist": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", "moist\u00a7": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", "motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", "moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0441\u0443\u0445", "not_moving": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", "not_occupied": "{entity_name} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "not_opened": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", "not_plugged_in": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", "not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", "not_present": "{entity_name} \u043d\u0435 \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", diff --git a/homeassistant/components/cert_expiry/.translations/bg.json b/homeassistant/components/cert_expiry/.translations/bg.json index 7c82ef8b9ba..a4a36cb04dc 100644 --- a/homeassistant/components/cert_expiry/.translations/bg.json +++ b/homeassistant/components/cert_expiry/.translations/bg.json @@ -4,10 +4,12 @@ "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" }, "error": { + "certificate_error": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d", "certificate_fetch_failed": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043c\u0438\u0437\u0432\u043b\u0435\u0447\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442", "connection_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441", "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" + "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d", + "wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u043d\u0435 \u0441\u044a\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0430 \u043d\u0430 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0445\u043e\u0441\u0442\u0430" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/bg.json b/homeassistant/components/coolmaster/.translations/bg.json new file mode 100644 index 00000000000..9e484f5d38c --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 CoolMasterNet. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430.", + "no_units": "\u041d\u0435 \u0431\u044f\u0445\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u0447\u043d\u0438/\u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043d\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0438\u044f CoolMasterNet \u0430\u0434\u0440\u0435\u0441." + }, + "step": { + "user": { + "data": { + "cool": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", + "dry": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u0438\u0437\u0441\u0443\u0448\u0430\u0432\u0430\u043d\u0435", + "fan_only": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440", + "heat": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", + "heat_cool": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435/\u043e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", + "host": "\u0410\u0434\u0440\u0435\u0441", + "off": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0432\u043e\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/bg.json b/homeassistant/components/cover/.translations/bg.json new file mode 100644 index 00000000000..4651fb4aebe --- /dev/null +++ b/homeassistant/components/cover/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u0435 \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "is_closing": "{entity_name} \u0441\u0435 \u0437\u0430\u0442\u0432\u0430\u0440\u044f", + "is_open": "{entity_name} \u0435 \u043e\u0442\u0432\u043e\u0440\u0435\u043d", + "is_opening": "{entity_name} \u0441\u0435 \u043e\u0442\u0432\u0430\u0440\u044f", + "is_position": "\u0422\u0435\u043a\u0443\u0449\u0430\u0442\u0430 \u043f\u043e\u0437\u0438\u0446\u0438\u044f \u043d\u0430 {entity_name} \u0435", + "is_tilt_position": "\u0422\u0435\u043a\u0443\u0449\u0430\u0442\u0430 \u043f\u043e\u0437\u0438\u0446\u0438\u044f \u043d\u0430 \u043d\u0430\u043a\u043b\u043e\u043d\u0430 \u043d\u0430 {entity_name} \u0435" + }, + "trigger_type": { + "closed": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "closing": "{entity_name} \u0441\u0435 \u0437\u0430\u0442\u0432\u0430\u0440\u044f", + "opened": "{entity_name} \u0435 \u043e\u0442\u0432\u043e\u0440\u0435\u043d", + "opening": "{entity_name} \u0441\u0435 \u043e\u0442\u0432\u0430\u0440\u044f", + "position": "{entity_name} \u043f\u0440\u043e\u043c\u0435\u043d\u0438 \u043f\u043e\u0437\u0438\u0446\u0438\u044f\u0442\u0430 \u0441\u0438", + "tilt_position": "{entity_name} \u043f\u0440\u043e\u043c\u0435\u043d\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430 \u0441\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json index da86e4960c0..b2c2371db5c 100644 --- a/homeassistant/components/cover/.translations/ca.json +++ b/homeassistant/components/cover/.translations/ca.json @@ -12,7 +12,9 @@ "closed": "{entity_name} tancat/da", "closing": "{entity_name} tancant-se", "opened": "{entity_name} s'ha obert", - "opening": "{entity_name} obrint-se" + "opening": "{entity_name} obrint-se", + "position": "Canvia la posici\u00f3 de {entity_name}", + "tilt_position": "Canvia la inclinaci\u00f3 de {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json index e9ed497ccc2..ba692f15e47 100644 --- a/homeassistant/components/cover/.translations/de.json +++ b/homeassistant/components/cover/.translations/de.json @@ -5,6 +5,12 @@ "is_closing": "{entity_name} wird geschlossen", "is_open": "{entity_name} ist offen", "is_opening": "{entity_name} wird ge\u00f6ffnet" + }, + "trigger_type": { + "closed": "{entity_name} geschlossen", + "closing": "{entity_name} wird geschlossen", + "opened": "{entity_name} ge\u00f6ffnet", + "opening": "{entity_name} wird ge\u00f6ffnet" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/it.json b/homeassistant/components/cover/.translations/it.json index 6a25c0f3f2f..bc9413d4a00 100644 --- a/homeassistant/components/cover/.translations/it.json +++ b/homeassistant/components/cover/.translations/it.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} \u00e8 chiuso", "is_closing": "{entity_name} si sta chiudendo", "is_open": "{entity_name} \u00e8 aperto", - "is_opening": "{entity_name} si sta aprendo" + "is_opening": "{entity_name} si sta aprendo", + "is_position": "La posizione attuale di {entity_name} \u00e8", + "is_tilt_position": "La posizione d'inclinazione attuale di {entity_name} \u00e8" + }, + "trigger_type": { + "closed": "{entity_name} chiuso", + "closing": "{entity_name} in chiusura", + "opened": "{entity_name} aperto", + "opening": "{entity_name} in apertura", + "position": "{entity_name} cambiamenti della posizione", + "tilt_position": "{entity_name} cambiamenti della posizione d'inclinazione" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 48f7ba17532..4839f8f034f 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", "is_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\ub294", "is_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\ub294" + }, + "trigger_type": { + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", + "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911", + "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", + "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911", + "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58 \ubcc0\ud654", + "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30 \ubcc0\ud654" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json index 901ececec1f..cc045e43624 100644 --- a/homeassistant/components/cover/.translations/no.json +++ b/homeassistant/components/cover/.translations/no.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} \u00e5pnes", "is_position": "{entity_name}-posisjonen er", "is_tilt_position": "{entity_name} vippeposisjon er" + }, + "trigger_type": { + "closed": "{entity_name} lukket", + "closing": "{entity_name} lukkes", + "opened": "{entity_name} \u00e5pnet", + "opening": "{entity_name} \u00e5pning", + "position": "{entity_name} posisjon endringer", + "tilt_position": "{entity_name} endringer i vippeposisjon" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json index 8197d2146ae..f2880a72e61 100644 --- a/homeassistant/components/cover/.translations/zh-Hant.json +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -5,8 +5,8 @@ "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589", "is_open": "{entity_name} \u5df2\u958b\u555f", "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f", - "is_position": "{entity_name} \u4f4d\u7f6e\u70ba", - "is_tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" + "is_position": "\u76ee\u524d {entity_name} \u4f4d\u7f6e\u70ba", + "is_tilt_position": "\u76ee\u524d {entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" }, "trigger_type": { "closed": "{entity_name} \u5df2\u95dc\u9589", diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index c9963e49623..e8dc5845c13 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -11,6 +11,7 @@ "error": { "no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447" }, + "flow_title": "deCONZ Zigbee \u0448\u043b\u044e\u0437 ({host})", "step": { "hassio_confirm": { "data": { @@ -64,6 +65,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", "remote_button_rotated": "\u0417\u0430\u0432\u044a\u0440\u0442\u044f\u043d \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", + "remote_button_rotation_stopped": "\u0421\u043f\u0440\u044f \u0432\u044a\u0440\u0442\u0435\u043d\u0435\u0442\u043e \u043d\u0430 \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 6d9f7236d31..e9c64ffe5fa 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -55,12 +55,6 @@ "left": "Left", "open": "Open", "right": "Right", - "side_1": "Side 1", - "side_2": "Side 2", - "side_3": "Side 3", - "side_4": "Side 4", - "side_5": "Side 5", - "side_6": "Side 6", "turn_off": "Turn off", "turn_on": "Turn on" }, @@ -75,17 +69,7 @@ "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_double_tap": "Device \"{subtype}\" double tapped", - "remote_awakened": "Device awakened", - "remote_falling": "Device in free fall", - "remote_gyro_activated": "Device shaken", - "remote_moved": "Device moved with \"{subtype}\" up", - "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", - "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", - "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", - "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", - "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", - "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" + "remote_gyro_activated": "Device shaken" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/bg.json b/homeassistant/components/device_tracker/.translations/bg.json new file mode 100644 index 00000000000..471cbc6a53a --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/bg.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u0435 \u0443 \u0434\u043e\u043c\u0430", + "is_not_home": "{entity_name} \u043d\u0435 \u0435 \u0443 \u0434\u043e\u043c\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/it.json b/homeassistant/components/device_tracker/.translations/it.json index 3030410a97a..e2d35296152 100644 --- a/homeassistant/components/device_tracker/.translations/it.json +++ b/homeassistant/components/device_tracker/.translations/it.json @@ -1,8 +1,8 @@ { "device_automation": { "condtion_type": { - "is_home": "{entity_name} \u00e8 a casa", - "is_not_home": "{entity_name} non \u00e8 a casa" + "is_home": "{entity_name} \u00e8 in casa", + "is_not_home": "{entity_name} non \u00e8 in casa" } } } \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/bg.json b/homeassistant/components/glances/.translations/bg.json new file mode 100644 index 00000000000..8604dda565a --- /dev/null +++ b/homeassistant/components/glances/.translations/bg.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430", + "wrong_version": "\u0412\u0435\u0440\u0441\u0438\u044f\u0442\u0430 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 (\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0438 \u0432\u0435\u0440\u0441\u0438\u0438: 2 \u0438\u043b\u0438 3)" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 SSL/TLS, \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u043a\u044a\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430 Glances", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430", + "version": "Glances API \u0432\u0435\u0440\u0441\u0438\u044f (2 \u0438\u043b\u0438 3)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/bg.json b/homeassistant/components/huawei_lte/.translations/bg.json new file mode 100644 index 00000000000..de5cbb32b79 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/bg.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "connection_failed": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "incorrect_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", + "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "incorrect_username_or_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430", + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0430\u0434\u0440\u0435\u0441", + "login_attempts_exceeded": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0438\u0442\u0435 \u043e\u043f\u0438\u0442\u0438 \u0437\u0430 \u0432\u043b\u0438\u0437\u0430\u043d\u0435 \u0441\u0430 \u043d\u0430\u0434\u0432\u0438\u0448\u0435\u043d\u0438. \u041c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e", + "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", + "unknown_connection_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "URL \u0410\u0434\u0440\u0435\u0441", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e. \u041f\u043e\u0441\u043e\u0447\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0435 \u0435 \u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e, \u043d\u043e \u0434\u0430\u0432\u0430 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0437\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435. \u041e\u0442 \u0434\u0440\u0443\u0433\u0430 \u0441\u0442\u0440\u0430\u043d\u0430, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0434\u043e\u0432\u0435\u0434\u0435 \u0434\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 \u0434\u043e\u0441\u0442\u044a\u043f\u0430 \u0434\u043e \u0443\u0435\u0431 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043e\u0442\u0432\u044a\u043d Home Assistant, \u0434\u043e\u043a\u0430\u0442\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0442\u043e.", + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 \u043d\u0430 SMS \u0438\u0437\u0432\u0435\u0441\u0442\u0438\u044f", + "track_new_devices": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index 4bfd3389745..0be2ed23462 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -8,7 +8,10 @@ "incorrect_password": "Password errata", "incorrect_username": "Nome utente errato", "incorrect_username_or_password": "Nome utente o password errati", - "invalid_url": "URL non valido" + "invalid_url": "URL non valido", + "login_attempts_exceeded": "Superati i tentativi di accesso massimi, riprovare pi\u00f9 tardi", + "response_error": "Errore sconosciuto dal dispositivo", + "unknown_connection_error": "Errore sconosciuto durante la connessione al dispositivo" }, "step": { "user": { @@ -16,14 +19,18 @@ "password": "Password", "url": "URL", "username": "Nome utente" - } + }, + "description": "Immettere i dettagli di accesso al dispositivo. La specifica di nome utente e password \u00e8 facoltativa, ma abilita il supporto per altre funzionalit\u00e0 di integrazione. D'altra parte, l'uso di una connessione autorizzata pu\u00f2 causare problemi di accesso all'interfaccia Web del dispositivo dall'esterno di Home Assistant mentre l'integrazione \u00e8 attiva e viceversa.", + "title": "Configura Huawei LTE" } - } + }, + "title": "Huawei LTE" }, "options": { "step": { "init": { "data": { + "recipient": "Destinatari della notifica SMS", "track_new_devices": "Traccia nuovi dispositivi" } } diff --git a/homeassistant/components/lock/.translations/bg.json b/homeassistant/components/lock/.translations/bg.json new file mode 100644 index 00000000000..0e77bcf1033 --- /dev/null +++ b/homeassistant/components/lock/.translations/bg.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "\u0417\u0430\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435 \u043d\u0430 {entity_name}", + "unlock": "\u041e\u0442\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "is_unlocked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/bg.json b/homeassistant/components/media_player/.translations/bg.json new file mode 100644 index 00000000000..f6c18cbe119 --- /dev/null +++ b/homeassistant/components/media_player/.translations/bg.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u0435 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u0435\u043d", + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_paused": "{entity_name} \u0435 \u043d\u0430 \u043f\u0430\u0443\u0437\u0430", + "is_playing": "{entity_name} \u0432\u044a\u0437\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0436\u0434\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/it.json b/homeassistant/components/media_player/.translations/it.json index 52a9bb3f051..93ab26d4585 100644 --- a/homeassistant/components/media_player/.translations/it.json +++ b/homeassistant/components/media_player/.translations/it.json @@ -1,9 +1,11 @@ { "device_automation": { "condition_type": { + "is_idle": "{entity_name} \u00e8 inattivo", "is_off": "{entity_name} \u00e8 spento", "is_on": "{entity_name} \u00e8 acceso", - "is_paused": "{entity_name} \u00e8 in pausa" + "is_paused": "{entity_name} \u00e8 in pausa", + "is_playing": "{entity_name} \u00e8 in esecuzione" } } } \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/bg.json b/homeassistant/components/neato/.translations/bg.json new file mode 100644 index 00000000000..14bc601214e --- /dev/null +++ b/homeassistant/components/neato/.translations/bg.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" + }, + "create_entry": { + "default": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url})." + }, + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438", + "unexpected_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "vendor": "\u0414\u043e\u0441\u0442\u0430\u0432\u0447\u0438\u043a" + }, + "description": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url}).", + "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 Neato \u0430\u043a\u0430\u0443\u043d\u0442" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/bg.json b/homeassistant/components/opentherm_gw/.translations/bg.json new file mode 100644 index 00000000000..cd109579f64 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/bg.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "already_configured": "\u0428\u043b\u044e\u0437\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440a \u043d\u0430 \u0448\u043b\u044e\u0437\u0430 \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430", + "serial_error": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435" + }, + "step": { + "init": { + "data": { + "device": "\u041f\u044a\u0442 \u0438\u043b\u0438 URL \u0430\u0434\u0440\u0435\u0441", + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430", + "id": "ID", + "name": "\u0418\u043c\u0435", + "precision": "\u041f\u0440\u0435\u0446\u0438\u0437\u043d\u043e\u0441\u0442 \u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430\u0442\u0430 \u043d\u0430 \u043a\u043b\u0438\u043c\u0430\u0442\u0430" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442" + }, + "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 OpenTherm Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json index fb77e5da8cb..9a2ffe299c8 100644 --- a/homeassistant/components/plex/.translations/bg.json +++ b/homeassistant/components/plex/.translations/bg.json @@ -4,7 +4,9 @@ "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "discovery_no_file": "\u041d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0441\u0442\u0430\u0440 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u0444\u0430\u0439\u043b", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", + "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { @@ -31,6 +33,10 @@ "description": "\u041d\u0430\u043b\u0438\u0447\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u044a\u0440\u0432\u044a\u0440\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d:", "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Plex \u0441\u044a\u0440\u0432\u044a\u0440" }, + "start_website_auth": { + "description": "\u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0435\u0442\u0435 \u0441 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 plex.tv.", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, "user": { "data": { "manual_setup": "\u0420\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", @@ -41,5 +47,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0432\u0441\u0438\u0447\u043a\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438", + "use_episode_art": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043b\u0430\u043a\u0430\u0442 \u0437\u0430 \u0435\u043f\u0438\u0437\u043e\u0434\u0430" + }, + "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 Plex Media Players" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/bg.json b/homeassistant/components/sensor/.translations/bg.json new file mode 100644 index 00000000000..fec70803486 --- /dev/null +++ b/homeassistant/components/sensor/.translations/bg.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "\u0422\u0435\u043a\u0443\u0449\u043e \u043d\u0438\u0432\u043e \u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u043d\u0430 {entity_name}", + "is_humidity": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", + "is_illuminance": "\u0422\u0435\u043a\u0443\u0449\u0430 \u043e\u0441\u0432\u0435\u0442\u0435\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", + "is_power": "\u0422\u0435\u043a\u0443\u0449\u0430 \u043c\u043e\u0449\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", + "is_pressure": "\u0422\u0435\u043a\u0443\u0449\u043e \u043d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "is_signal_strength": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0438\u043b\u0430 \u043d\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430 \u043d\u0430 {entity_name}", + "is_temperature": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 {entity_name}", + "is_timestamp": "\u0422\u0435\u043a\u0443\u0449\u043e \u0432\u0440\u0435\u043c\u0435 \u043d\u0430 {entity_name}", + "is_value": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}" + }, + "trigger_type": { + "battery_level": "{entity_name} \u043d\u0438\u0432\u043e\u0442\u043e \u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u044f", + "humidity": "{entity_name} \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "illuminance": "{entity_name} \u043e\u0441\u0432\u0435\u0442\u0435\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "power": "\u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "pressure": "\u043d\u0430\u043b\u044f\u0433\u0430\u043d\u0435\u0442\u043e \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "signal_strength": "\u0441\u0438\u043b\u0430\u0442\u0430 \u043d\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "temperature": "\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "timestamp": "\u0432\u0440\u0435\u043c\u0435\u0442\u043e \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "value": "\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/bg.json b/homeassistant/components/solarlog/.translations/bg.json new file mode 100644 index 00000000000..6dabc169f12 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435, \u043c\u043e\u043b\u044f, \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 \u0412\u0430\u0448\u0435\u0442\u043e Solar-Log \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "name": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441\u044a\u0442, \u043a\u043e\u0439\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u0432\u0430\u0448\u0438\u0442\u0435 Solar-Log \u0441\u0435\u043d\u0437\u043e\u0440\u0438" + }, + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0441\u0438 \u0441\u044a\u0441 Solar-log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/bg.json b/homeassistant/components/soma/.translations/bg.json new file mode 100644 index 00000000000..0b7dd3b689f --- /dev/null +++ b/homeassistant/components/soma/.translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Soma \u0430\u043a\u0430\u0443\u043d\u0442.", + "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 Soma \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441\u044a\u0441 Soma." + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f SOMA Connect.", + "title": "SOMA Connect" + } + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/bg.json b/homeassistant/components/somfy/.translations/bg.json index 1e47a7bae45..4741ccdba6d 100644 --- a/homeassistant/components/somfy/.translations/bg.json +++ b/homeassistant/components/somfy/.translations/bg.json @@ -8,6 +8,11 @@ "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441\u044a\u0441 Somfy." }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/it.json b/homeassistant/components/somfy/.translations/it.json index 06fc8bed40f..c91536abc77 100644 --- a/homeassistant/components/somfy/.translations/it.json +++ b/homeassistant/components/somfy/.translations/it.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Autenticato con successo con Somfy." }, + "step": { + "pick_implementation": { + "title": "Seleziona il metodo di autenticazione" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/bg.json b/homeassistant/components/transmission/.translations/bg.json new file mode 100644 index 00000000000..98160b89925 --- /dev/null +++ b/homeassistant/components/transmission/.translations/bg.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d.", + "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430", + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430", + "wrong_credentials": "\u0413\u0440\u0435\u0448\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + }, + "step": { + "options": { + "data": { + "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" + }, + "title": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435" + }, + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Transmission \u043a\u043b\u0438\u0435\u043d\u0442" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 Transmission", + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/bg.json b/homeassistant/components/unifi/.translations/bg.json index df5654ff78b..4b8b8977491 100644 --- a/homeassistant/components/unifi/.translations/bg.json +++ b/homeassistant/components/unifi/.translations/bg.json @@ -32,6 +32,11 @@ "track_devices": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (Ubiquiti \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u043a\u0430\u0431\u0435\u043b" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0421\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0441\u0435\u043d\u0437\u043e\u0440\u0438 \u0437\u0430 \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0438" + } } } } diff --git a/homeassistant/components/withings/.translations/bg.json b/homeassistant/components/withings/.translations/bg.json index e75860d0e16..4064b21ca6b 100644 --- a/homeassistant/components/withings/.translations/bg.json +++ b/homeassistant/components/withings/.translations/bg.json @@ -7,6 +7,13 @@ "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Withings \u0437\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b." }, "step": { + "profile": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" + }, + "description": "\u041a\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b \u0441\u0442\u0435 \u0438\u0437\u0431\u0440\u0430\u043b\u0438 \u043d\u0430 \u0443\u0435\u0431\u0441\u0430\u0439\u0442\u0430 \u043d\u0430 Withings? \u0412\u0430\u0436\u043d\u043e \u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0438\u0442\u0435 \u0434\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0430\u0442, \u0432 \u043f\u0440\u043e\u0442\u0438\u0432\u0435\u043d \u0441\u043b\u0443\u0447\u0430\u0439 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0449\u0435 \u0431\u044a\u0434\u0430\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438.", + "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b." + }, "user": { "data": { "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index fbd590697e1..4ac73dde195 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -11,6 +11,7 @@ "data": { "profile": "Profilo" }, + "description": "Quale profilo hai selezionato sul sito web di Withings? \u00c8 importante che i profili corrispondano, altrimenti i dati avranno con un'errata etichettatura.", "title": "Profilo utente." }, "user": { diff --git a/homeassistant/components/zha/.translations/bg.json b/homeassistant/components/zha/.translations/bg.json index 2715ef46dc8..916d09a6830 100644 --- a/homeassistant/components/zha/.translations/bg.json +++ b/homeassistant/components/zha/.translations/bg.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "\u041a\u0432\u0430\u043a", + "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435" + }, "trigger_subtype": { "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", From 512ef2fce46be2e47e6e070f18ce45b076f889a3 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Sat, 2 Nov 2019 02:32:37 -0700 Subject: [PATCH 126/306] Change ps4 state off to state standby (#28261) * Change state off to state standby * update docstring --- homeassistant/components/ps4/media_player.py | 12 +++++------ tests/components/ps4/test_media_player.py | 22 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 3e8b667cd13..91d3a5b13c7 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -26,7 +26,7 @@ from homeassistant.const import ( CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_OFF, + STATE_STANDBY, STATE_PLAYING, ) from homeassistant.helpers import device_registry, entity_registry @@ -201,8 +201,8 @@ class PS4Device(MediaPlayerDevice): if self._state != STATE_IDLE: self.idle() else: - if self._state != STATE_OFF: - self.state_off() + if self._state != STATE_STANDBY: + self.state_standby() elif self._retry > DEFAULT_RETRIES: self.state_unknown() @@ -230,10 +230,10 @@ class PS4Device(MediaPlayerDevice): self._state = STATE_IDLE self.schedule_update() - def state_off(self): - """Set states for state off.""" + def state_standby(self): + """Set states for state standby.""" self.reset_title() - self._state = STATE_OFF + self._state = STATE_STANDBY self.schedule_update() def state_unknown(self): diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 7bf93e37777..e2b9e382dc4 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -29,7 +29,7 @@ from homeassistant.const import ( CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_OFF, + STATE_STANDBY, STATE_PLAYING, STATE_UNKNOWN, ) @@ -46,7 +46,7 @@ MOCK_HOST_NAME = "Fake PS4" MOCK_HOST_ID = "A0000A0AA000" MOCK_HOST_VERSION = "09879011" MOCK_HOST_TYPE = "PS4" -MOCK_STATUS_STANDBY = "Server Standby" +MOCK_STATUS_REST = "Server Standby" MOCK_STATUS_ON = "Ok" MOCK_STANDBY_CODE = 620 MOCK_ON_CODE = 200 @@ -100,13 +100,13 @@ MOCK_STATUS_IDLE = { "system-version": MOCK_HOST_VERSION, } -MOCK_STATUS_OFF = { +MOCK_STATUS_STANDBY = { "host-type": MOCK_HOST_TYPE, "host-ip": MOCK_HOST, "host-request-port": MOCK_TCP_PORT, "host-id": MOCK_HOST_ID, "host-name": MOCK_HOST_NAME, - "status": MOCK_STATUS_STANDBY, + "status": MOCK_STATUS_REST, "status_code": MOCK_STANDBY_CODE, "device-discovery-protocol-version": MOCK_DDP_VERSION, "system-version": MOCK_HOST_VERSION, @@ -183,14 +183,14 @@ async def test_media_player_is_setup_correctly_with_entry(hass): assert mock_state == STATE_UNKNOWN -async def test_state_off_is_set(hass): - """Test that state is set to off.""" +async def test_state_standby_is_set(hass): + """Test that state is set to standby.""" mock_entity_id = await setup_mock_component(hass) with patch(MOCK_SAVE, side_effect=MagicMock()): - await mock_ddp_response(hass, MOCK_STATUS_OFF) + await mock_ddp_response(hass, MOCK_STATUS_STANDBY) - assert hass.states.get(mock_entity_id).state == STATE_OFF + assert hass.states.get(mock_entity_id).state == STATE_STANDBY async def test_state_playing_is_set(hass): @@ -290,13 +290,13 @@ async def test_media_attributes_are_loaded(hass): async def test_device_info_is_set_from_status_correctly(hass): """Test that device info is set correctly from status update.""" mock_d_registry = mock_device_registry(hass) - with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_OFF): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_STANDBY): mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() # Reformat mock status-sw_version for assertion. - mock_version = MOCK_STATUS_OFF["system-version"] + mock_version = MOCK_STATUS_STANDBY["system-version"] mock_version = mock_version[1:4] mock_version = "{}.{}".format(mock_version[0], mock_version[1:]) @@ -306,7 +306,7 @@ async def test_device_info_is_set_from_status_correctly(hass): mock_entry = mock_d_registry.async_get_device( identifiers={(DOMAIN, MOCK_HOST_ID)}, connections={()} ) - assert mock_state == STATE_OFF + assert mock_state == STATE_STANDBY assert len(mock_d_entries) == 1 assert mock_entry.name == MOCK_HOST_NAME From b8fa5367dbbdb2d11f1a0563535e9229b4749025 Mon Sep 17 00:00:00 2001 From: Nash Kaminski Date: Sat, 2 Nov 2019 04:51:30 -0500 Subject: [PATCH 127/306] Fix inability to transition between specific presets in Venstar component (#28238) This change addresses a bug where one is unable to change directly between the away and temperature hold presets, as temperature hold cannot be enabled on a Venstar thermostat if away mode is active. Furthermore, this change removes redundant state checks as the set_away and set_schedule calls are idempotent in the venstarcolortouch library. See https://github.com/hpeyerl/venstar_colortouch/blob/master/src/venstarcolortouch/venstarcolortouch.py#L275. --- homeassistant/components/venstar/climate.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 9e5450addc5..c948772197f 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -247,6 +247,7 @@ class VenstarThermostat(ClimateDevice): return PRESET_AWAY if self._client.schedule == 0: return HOLD_MODE_TEMPERATURE + return PRESET_NONE @property def preset_modes(self): @@ -332,13 +333,11 @@ class VenstarThermostat(ClimateDevice): if preset_mode == PRESET_AWAY: success = self._client.set_away(self._client.AWAY_AWAY) elif preset_mode == HOLD_MODE_TEMPERATURE: - success = self._client.set_schedule(0) + success = self._client.set_away(self._client.AWAY_HOME) + success = success and self._client.set_schedule(0) elif preset_mode == PRESET_NONE: - success = False - if self._client.away: - success = self._client.set_away(self._client.AWAY_HOME) - if self._client.schedule == 0: - success = success and self._client.set_schedule(1) + success = self._client.set_away(self._client.AWAY_HOME) + success = success and self._client.set_schedule(1) else: _LOGGER.error("Unknown hold mode: %s", preset_mode) success = False From a8dff2f2d0b2c33a7e47a06d51248c3d73f6888e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 2 Nov 2019 21:21:09 +0200 Subject: [PATCH 128/306] pre-commit: ship default and full configs (#28463) For now, the only difference between the two is mypy. --- .pre-commit-config-all.yaml | 42 +++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 21 +++++++------------ azure-pipelines-ci.yml | 8 +++---- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- tox.ini | 2 +- 6 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 .pre-commit-config-all.yaml diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml new file mode 100644 index 00000000000..98829e25fc3 --- /dev/null +++ b/.pre-commit-config-all.yaml @@ -0,0 +1,42 @@ +# This configuration includes the full set of hooks we use. In +# addition to the defaults (see .pre-commit-config.yaml), this +# includes hooks that require our development and test dependencies +# installed and the virtualenv containing them active by the time +# pre-commit runs to produce correct results. +# +# If this is not a problem for your workflow, using this config is +# recommended, install it with +# pre-commit install --config .pre-commit-config-all.yaml +# Otherwise, see the default .pre-commit-config.yaml for a lighter one. + +repos: +- repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + args: + - --safe + - --quiet + files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.8 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.3.1 + - pydocstyle==4.0.0 + files: ^(homeassistant|script|tests)/.+\.py$ +# Using a local "system" mypy instead of the mypy hook, because its +# results depend on what is installed. And the mypy hook runs in a +# virtualenv of its own, meaning we'd need to install and maintain +# another set of our dependencies there... no. Use the "system" one +# and reuse the environment that is set up anyway already instead. +- repo: local + hooks: + - id: mypy + name: mypy + entry: mypy + language: system + types: [python] + require_serial: true + files: ^homeassistant/.+\.py$ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2b99393639..4beff14965b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,10 @@ +# This configuration includes the default, minimal set of hooks to be +# run on all commits. It requires no specific setup and one can just +# start using pre-commit with it. +# +# See .pre-commit-config-all.yaml for a more complete one that comes +# with a better coverage at the cost of some specific setup needed. + repos: - repo: https://github.com/psf/black rev: 19.10b0 @@ -15,17 +22,3 @@ repos: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 files: ^(homeassistant|script|tests)/.+\.py$ -# Using a local "system" mypy instead of the mypy hook, because its -# results depend on what is installed. And the mypy hook runs in a -# virtualenv of its own, meaning we'd need to install and maintain -# another set of our dependencies there... no. Use the "system" one -# and reuse the environment that is set up anyway already instead. -- repo: local - hooks: - - id: mypy - name: mypy - entry: mypy - language: system - types: [python] - require_serial: true - files: ^homeassistant/.+\.py$ diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index f1abf2ff9db..e80f4b8d0ba 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -45,7 +45,7 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks + pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate pre-commit run flake8 --all-files @@ -84,7 +84,7 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks + pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate pre-commit run black --all-files @@ -182,8 +182,8 @@ stages: . venv/bin/activate pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks + pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate - pre-commit run mypy --all-files + pre-commit run --config .pre-commit-config-all.yaml mypy --all-files displayName: 'Run mypy' diff --git a/requirements_test.txt b/requirements_test.txt index f2935a423bf..06a2ef1621d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ # make new things fail. Manually update these pins when pulling in a # new version -# When updating this file, update .pre-commit-config.yaml too +# When updating this file, update .pre-commit-config*.yaml too asynctest==0.13.0 black==19.10b0 codecov==2.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e757272a789..d7b4e4ffb01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -3,7 +3,7 @@ # make new things fail. Manually update these pins when pulling in a # new version -# When updating this file, update .pre-commit-config.yaml too +# When updating this file, update .pre-commit-config*.yaml too asynctest==0.13.0 black==19.10b0 codecov==2.0.15 diff --git a/tox.ini b/tox.ini index f6d12fe30f5..118d0722b7c 100644 --- a/tox.ini +++ b/tox.ini @@ -41,4 +41,4 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pre-commit run mypy {posargs: --all-files} + pre-commit run --config .pre-commit-config-all.yaml mypy {posargs: --all-files} From 1679ec3245c88c8abb52af8cc5bf212bb98e48ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 2 Nov 2019 21:30:09 +0200 Subject: [PATCH 129/306] SSDP matching improvements (#28285) * SSDP matching improvements - support multiple match groups per domain - require matches on all, not any item in a group - support matching on all UPnP device description data * Manifest structure fixes --- homeassistant/components/deconz/manifest.json | 10 ++-- homeassistant/components/heos/manifest.json | 12 ++-- homeassistant/components/hue/manifest.json | 10 ++-- homeassistant/components/sonos/manifest.json | 10 ++-- homeassistant/components/ssdp/__init__.py | 31 +++++----- homeassistant/components/wemo/manifest.json | 10 ++-- homeassistant/generated/ssdp.py | 43 ++++++++------ script/hassfest/manifest.py | 6 +- script/hassfest/ssdp.py | 16 ++---- tests/components/ssdp/test_init.py | 57 ++++++++++++++----- 10 files changed, 113 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 63ab17d001a..64902002600 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -6,11 +6,11 @@ "requirements": [ "pydeconz==64" ], - "ssdp": { - "manufacturer": [ - "Royal Philips Electronics" - ] - }, + "ssdp": [ + { + "manufacturer": "Royal Philips Electronics" + } + ], "dependencies": [], "codeowners": [ "@kane610" diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index fb21a43356f..684127e519e 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -6,13 +6,13 @@ "requirements": [ "pyheos==0.6.0" ], - "ssdp": { - "st": [ - "urn:schemas-denon-com:device:ACT-Denon:1" - ] - }, + "ssdp": [ + { + "st": "urn:schemas-denon-com:device:ACT-Denon:1" + } + ], "dependencies": [], "codeowners": [ "@andrewsayre" ] -} \ No newline at end of file +} diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 9a3e478d108..c90b6181559 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -6,11 +6,11 @@ "requirements": [ "aiohue==1.9.2" ], - "ssdp": { - "manufacturer": [ - "Royal Philips Electronics" - ] - }, + "ssdp": [ + { + "manufacturer": "Royal Philips Electronics" + } + ], "homekit": { "models": [ "BSB002" diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 7b0c041b2a9..46723bdcf5f 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -7,10 +7,10 @@ "pysonos==0.0.24" ], "dependencies": [], - "ssdp": { - "st": [ - "urn:schemas-upnp-org:device:ZonePlayer:1" - ] - }, + "ssdp": [ + { + "st": "urn:schemas-upnp-org:device:ZonePlayer:1" + } + ], "codeowners": [] } diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index d2382748f30..c4d71e0febd 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -104,28 +104,27 @@ class Scanner: async def _process_entry(self, entry): """Process a single entry.""" - domains = set(SSDP["st"].get(entry.st, [])) - xml_location = entry.location + info = {"st": entry.st} - if not xml_location: - if domains: - return (entry, info_from_entry(entry, None), domains) - return None + if entry.location: - # Multiple entries usually share same location. Make sure - # we fetch it only once. - info_req = self._description_cache.get(xml_location) + # Multiple entries usually share same location. Make sure + # we fetch it only once. + info_req = self._description_cache.get(entry.location) - if info_req is None: - info_req = self._description_cache[ - xml_location - ] = self.hass.async_create_task(self._fetch_description(xml_location)) + if info_req is None: + info_req = self._description_cache[ + entry.location + ] = self.hass.async_create_task(self._fetch_description(entry.location)) - info = await info_req + info.update(await info_req) - domains.update(SSDP["manufacturer"].get(info.get("manufacturer"), [])) - domains.update(SSDP["device_type"].get(info.get("deviceType"), [])) + domains = set() + for domain, matchers in SSDP.items(): + for matcher in matchers: + if all(info.get(k) == v for (k, v) in matcher.items()): + domains.add(domain) if domains: return (entry, info_from_entry(entry, info), domains) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index aa863bcff0d..3b43def230f 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -6,11 +6,11 @@ "requirements": [ "pywemo==0.4.34" ], - "ssdp": { - "manufacturer": [ - "Belkin International Inc." - ] - }, + "ssdp": [ + { + "manufacturer": "Belkin International Inc." + } + ], "homekit": { "models": [ "Wemo" diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 6d62c47110b..472ad6683ed 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -6,22 +6,29 @@ To update, run python3 -m script.hassfest # fmt: off SSDP = { - "device_type": {}, - "manufacturer": { - "Belkin International Inc.": [ - "wemo" - ], - "Royal Philips Electronics": [ - "deconz", - "hue" - ] - }, - "st": { - "urn:schemas-denon-com:device:ACT-Denon:1": [ - "heos" - ], - "urn:schemas-upnp-org:device:ZonePlayer:1": [ - "sonos" - ] - } + "deconz": [ + { + "manufacturer": "Royal Philips Electronics" + } + ], + "heos": [ + { + "st": "urn:schemas-denon-com:device:ACT-Denon:1" + } + ], + "hue": [ + { + "manufacturer": "Royal Philips Electronics" + } + ], + "sonos": [ + { + "st": "urn:schemas-upnp-org:device:ZonePlayer:1" + } + ], + "wemo": [ + { + "manufacturer": "Belkin International Inc." + } + ] } diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5cf8772686e..16f8b77b5d3 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -14,11 +14,7 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("config_flow"): bool, vol.Optional("zeroconf"): [str], vol.Optional("ssdp"): vol.Schema( - { - vol.Optional("st"): [str], - vol.Optional("manufacturer"): [str], - vol.Optional("device_type"): [str], - } + vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), vol.Required("documentation"): str, diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 3b02ea18151..d2dd724605e 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -24,11 +24,8 @@ def sort_dict(value): def generate_and_validate(integrations: Dict[str, Integration]): """Validate and generate ssdp data.""" - data = { - "st": defaultdict(list), - "manufacturer": defaultdict(list), - "device_type": defaultdict(list), - } + + data = defaultdict(list) for domain in sorted(integrations): integration = integrations[domain] @@ -56,14 +53,9 @@ def generate_and_validate(integrations: Dict[str, Integration]): ) continue - for key in "st", "manufacturer", "device_type": - if key not in ssdp: - continue + for matcher in ssdp: + data[domain].append(sort_dict(matcher)) - for value in ssdp[key]: - data[key][value].append(domain) - - data = sort_dict({key: sort_dict(value) for key, value in data.items()}) return BASE.format(json.dumps(data, indent=4)) diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 81c0f886b41..56b937cf9d9 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -17,7 +17,7 @@ async def test_scan_match_st(hass): with patch( "netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location=None)] - ), patch.dict(gn_ssdp.SSDP["st"], {"mock-st": ["mock-domain"]}), patch.object( + ), patch.dict(gn_ssdp.SSDP, {"mock-domain": [{"st": "mock-st"}]}), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: await scanner.async_scan(None) @@ -27,14 +27,15 @@ async def test_scan_match_st(hass): assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} -async def test_scan_match_manufacturer(hass, aioclient_mock): - """Test matching based on ST.""" +@pytest.mark.parametrize("key", ("manufacturer", "deviceType")) +async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): + """Test matching based on UPnP device description data.""" aioclient_mock.get( "http://1.1.1.1", - text=""" + text=f""" - Paulus + <{key}>Paulus """, @@ -44,9 +45,7 @@ async def test_scan_match_manufacturer(hass, aioclient_mock): with patch( "netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location="http://1.1.1.1")], - ), patch.dict( - gn_ssdp.SSDP["manufacturer"], {"Paulus": ["mock-domain"]} - ), patch.object( + ), patch.dict(gn_ssdp.SSDP, {"mock-domain": [{key: "Paulus"}]}), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: await scanner.async_scan(None) @@ -56,11 +55,11 @@ async def test_scan_match_manufacturer(hass, aioclient_mock): assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} -async def test_scan_match_device_type(hass, aioclient_mock): - """Test matching based on ST.""" +async def test_scan_not_all_present(hass, aioclient_mock): + """Test match fails if some specified attributes are not present.""" aioclient_mock.get( "http://1.1.1.1", - text=""" + text=f""" Paulus @@ -74,15 +73,43 @@ async def test_scan_match_device_type(hass, aioclient_mock): "netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location="http://1.1.1.1")], ), patch.dict( - gn_ssdp.SSDP["device_type"], {"Paulus": ["mock-domain"]} + gn_ssdp.SSDP, + {"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Paulus"}]}, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: await scanner.async_scan(None) - assert len(mock_init.mock_calls) == 1 - assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert not mock_init.mock_calls + + +async def test_scan_not_all_match(hass, aioclient_mock): + """Test match fails if some specified attribute values differ.""" + aioclient_mock.get( + "http://1.1.1.1", + text=f""" + + + Paulus + Paulus + + + """, + ) + scanner = ssdp.Scanner(hass) + + with patch( + "netdisco.ssdp.scan", + return_value=[Mock(st="mock-st", location="http://1.1.1.1")], + ), patch.dict( + gn_ssdp.SSDP, + {"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Not-Paulus"}]}, + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert not mock_init.mock_calls @pytest.mark.parametrize("exc", [asyncio.TimeoutError, aiohttp.ClientError]) From f71527d5dbec1f34f26e55d41cd325c531dbf74a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 3 Nov 2019 00:31:44 +0000 Subject: [PATCH 130/306] [ci skip] Translation update --- .../components/almond/.translations/sl.json | 9 +++++ .../cert_expiry/.translations/sl.json | 4 +- .../coolmaster/.translations/sl.json | 23 +++++++++++ .../components/cover/.translations/fr.json | 8 ++++ .../components/cover/.translations/ko.json | 4 +- .../components/cover/.translations/sl.json | 12 +++++- .../device_tracker/.translations/ko.json | 4 +- .../device_tracker/.translations/sl.json | 8 ++++ .../huawei_lte/.translations/nl.json | 10 +++++ .../huawei_lte/.translations/sl.json | 39 +++++++++++++++++++ .../media_player/.translations/sl.json | 11 ++++++ .../components/sensor/.translations/fr.json | 6 +-- .../components/sensor/.translations/sl.json | 36 ++++++++--------- .../components/solarlog/.translations/sl.json | 21 ++++++++++ .../components/somfy/.translations/sl.json | 5 +++ .../transmission/.translations/sl.json | 5 ++- .../components/withings/.translations/sl.json | 7 ++++ 17 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/almond/.translations/sl.json create mode 100644 homeassistant/components/coolmaster/.translations/sl.json create mode 100644 homeassistant/components/device_tracker/.translations/sl.json create mode 100644 homeassistant/components/huawei_lte/.translations/nl.json create mode 100644 homeassistant/components/huawei_lte/.translations/sl.json create mode 100644 homeassistant/components/media_player/.translations/sl.json create mode 100644 homeassistant/components/solarlog/.translations/sl.json diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json new file mode 100644 index 00000000000..f8bf3021db5 --- /dev/null +++ b/homeassistant/components/almond/.translations/sl.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo en ra\u010dun Almond.", + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json index 3774956330a..d375c626c66 100644 --- a/homeassistant/components/cert_expiry/.translations/sl.json +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -4,10 +4,12 @@ "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana" }, "error": { + "certificate_error": "Certifikata ni bilo mogo\u010de preveriti", "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla", "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", - "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" + "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti", + "wrong_host": "Potrdilo se ne ujema z imenom gostitelja" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/sl.json b/homeassistant/components/coolmaster/.translations/sl.json new file mode 100644 index 00000000000..a59b5215e7f --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Povezava s CoolMasterNet ni uspela. Preverite svojega gostitelja.", + "no_units": "V gostitelju CoolMasterNet ni bilo mogo\u010de najti nobenih enot HVAC." + }, + "step": { + "user": { + "data": { + "cool": "Podpira na\u010din hlajenja", + "dry": "Podpira na\u010din su\u0161enja", + "fan_only": "Podpira samo na\u010din ventilacije", + "heat": "Podpira na\u010din ogrevanja", + "heat_cool": "Podpira samodejni na\u010din ogrevanja / hlajenja", + "host": "Gostitelj", + "off": "Lahko se izklopi" + }, + "title": "Nastavite svoje podatke CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json index 1f2debe9c04..3aa877637d9 100644 --- a/homeassistant/components/cover/.translations/fr.json +++ b/homeassistant/components/cover/.translations/fr.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} est en train de s'ouvrir", "is_position": "La position de {entity_name} est", "is_tilt_position": "La position d'inclinaison de {entity_name} est" + }, + "trigger_type": { + "closed": "{entity_name} ferm\u00e9", + "closing": "{entity_name} fermeture", + "opened": "{entity_name} ouvert", + "opening": "{entity_name} ouverture", + "position": "{entity_name} changement de position", + "tilt_position": "{entity_name} changement d'inclinaison" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 4839f8f034f..6a59bb9f6ae 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -5,8 +5,8 @@ "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", - "is_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\ub294", - "is_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\ub294" + "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58", + "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30" }, "trigger_type": { "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", diff --git a/homeassistant/components/cover/.translations/sl.json b/homeassistant/components/cover/.translations/sl.json index cb5109b5cb0..cd3570d39ba 100644 --- a/homeassistant/components/cover/.translations/sl.json +++ b/homeassistant/components/cover/.translations/sl.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} je/so zaprt/a", "is_closing": "{entity_name} se zapira/jo", "is_open": "{entity_name} je odprt/a/o", - "is_opening": "{entity_name} se odpira/jo" + "is_opening": "{entity_name} se odpira/jo", + "is_position": "Trenutna pozicija {entity_name} je", + "is_tilt_position": "Trenutni polo\u017eaj nagiba {entity_name} je" + }, + "trigger_type": { + "closed": "{entity_name} se je/so se zaprla", + "closing": "{entity_name} se zapira/jo", + "opened": "{entity_name} se/so je odprla", + "opening": "{entity_name} se odpira/jo", + "position": "{entity_name} spremembe polo\u017eaja", + "tilt_position": "{entity_name} spremembe nagiba" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ko.json b/homeassistant/components/device_tracker/.translations/ko.json index 34389297f28..d258f67db22 100644 --- a/homeassistant/components/device_tracker/.translations/ko.json +++ b/homeassistant/components/device_tracker/.translations/ko.json @@ -1,8 +1,8 @@ { "device_automation": { "condtion_type": { - "is_home": "{entity_name} \ub2d8\uc774 \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", - "is_not_home": "{entity_name} \ub2d8\uc774 \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" + "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", + "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/sl.json b/homeassistant/components/device_tracker/.translations/sl.json new file mode 100644 index 00000000000..f4784fbc664 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/sl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} je doma", + "is_not_home": "{entity_name} ni doma" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/nl.json b/homeassistant/components/huawei_lte/.translations/nl.json new file mode 100644 index 00000000000..4e4b63e9391 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "config": { + "error": { + "incorrect_password": "Onjuist wachtwoord", + "incorrect_username": "Onjuiste gebruikersnaam", + "incorrect_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", + "invalid_url": "Ongeldige URL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/sl.json b/homeassistant/components/huawei_lte/.translations/sl.json new file mode 100644 index 00000000000..e23ac72bcca --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/sl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Ta naprava je \u017ee nastavljena" + }, + "error": { + "connection_failed": "Povezava ni uspela", + "incorrect_password": "Nepravilno geslo", + "incorrect_username": "Nepravilno uporabni\u0161ko ime", + "incorrect_username_or_password": "Nepravilno uporabni\u0161ko ime ali geslo", + "invalid_url": "Neveljaven URL", + "login_attempts_exceeded": "Najve\u010d poskusov prijave prese\u017eeno, prosimo, poskusite znova pozneje", + "response_error": "Neznana napaka iz naprave", + "unknown_connection_error": "Neznana napaka pri povezovanju z napravo" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "url": "URL", + "username": "Uporabni\u0161ko ime" + }, + "description": "Vnesite podatke za dostop do naprave. Dolo\u010danje uporabni\u0161kega imena in gesla je izbirno, vendar omogo\u010da podporo za ve\u010d funkcij integracije. Po drugi strani pa lahko uporaba poobla\u0161\u010dene povezave povzro\u010di te\u017eave pri dostopu do spletnega vmesnika naprave zunaj Home Assistant-a, medtem ko je integracija aktivna, in obratno.", + "title": "Konfigurirajte Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Prejemniki obvestil SMS", + "track_new_devices": "Sledi novim napravam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/sl.json b/homeassistant/components/media_player/.translations/sl.json new file mode 100644 index 00000000000..eafc10bd8ab --- /dev/null +++ b/homeassistant/components/media_player/.translations/sl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} je nedejaven", + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", + "is_paused": "{entity_name} je zaustavljen", + "is_playing": "{entity_name} predvaja" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index f0060f151dc..1ce2592410d 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -1,9 +1,9 @@ { "device_automation": { "condition_type": { - "is_battery_level": "Le niveau de la batterie de {entity_name}", - "is_humidity": "L'humidit\u00e9 de {entity_name}", - "is_illuminance": "L'\u00e9clairement de {entity_name}", + "is_battery_level": "Niveau de la batterie de {entity_name}", + "is_humidity": "Humidit\u00e9 de {entity_name}", + "is_illuminance": "\u00c9clairement de {entity_name}", "is_power": "{entity_name} puissance", "is_pressure": "{entity_name} pression", "is_signal_strength": "{entity_name} force du signal", diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json index e3bc994b6ea..3f29b62e665 100644 --- a/homeassistant/components/sensor/.translations/sl.json +++ b/homeassistant/components/sensor/.translations/sl.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} raven baterije", - "is_humidity": "{entity_name} vla\u017enost", - "is_illuminance": "{entity_name} osvetlitev", - "is_power": "{entity_name} mo\u010d", - "is_pressure": "{entity_name} pritisk", - "is_signal_strength": "{entity_name} jakost signala", - "is_temperature": "{entity_name} temperatura", - "is_timestamp": "{entity_name} \u010dasovni \u017eig", - "is_value": "{entity_name} vrednost" + "is_battery_level": "Trenutna raven baterije {entity_name}", + "is_humidity": "Trenutna vla\u017enost {entity_name}", + "is_illuminance": "Trenutna svetilnost {entity_name}", + "is_power": "Trenutna mo\u010d {entity_name}", + "is_pressure": "Trenutni tlak {entity_name}", + "is_signal_strength": "Trenutna jakost signala {entity_name}", + "is_temperature": "Trenutna temperatura {entity_name}", + "is_timestamp": "Trenutni {entity_name} \u010dasovni \u017eig", + "is_value": "Trenutna vrednost {entity_name}" }, "trigger_type": { - "battery_level": "{entity_name} raven baterije", - "humidity": "{entity_name} vla\u017enost", - "illuminance": "{entity_name} osvetljenosti", - "power": "{entity_name} mo\u010d", - "pressure": "{entity_name} tlak", - "signal_strength": "{entity_name} jakost signala", - "temperature": "{entity_name} temperatura", - "timestamp": "{entity_name} \u010dasovni \u017eig", - "value": "{entity_name} vrednost" + "battery_level": "{entity_name} spremembe ravni baterije", + "humidity": "{entity_name} spremembe vla\u017enosti", + "illuminance": "{entity_name} spremembe osvetlitve", + "power": "{entity_name} spremembe mo\u010di", + "pressure": "{entity_name} spremembe tlaka", + "signal_strength": "{entity_name} spremembe mo\u010di signala", + "temperature": "{entity_name} spremembe temperatur", + "timestamp": "{entity_name} spremembe \u010dasovnega \u017eiga", + "value": "{entity_name} spremembe vrednosti" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/sl.json b/homeassistant/components/solarlog/.translations/sl.json new file mode 100644 index 00000000000..152152eacf7 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "already_configured": "Naprava je \u017ee konfigurirana", + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave, preverite naslov gostitelja" + }, + "step": { + "user": { + "data": { + "host": "Ime gostitelja ali ip naslov va\u0161e naprave Solar-Log", + "name": "Predpona, ki jo \u017eelite uporabiti za senzorje Solar-log" + }, + "title": "Dolo\u010dite povezavo Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/sl.json b/homeassistant/components/somfy/.translations/sl.json index 87e8e33c814..bdfbade2bbe 100644 --- a/homeassistant/components/somfy/.translations/sl.json +++ b/homeassistant/components/somfy/.translations/sl.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Uspe\u0161no overjen s Somfy-jem." }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sl.json b/homeassistant/components/transmission/.translations/sl.json index 122c332f429..37ce27e19f4 100644 --- a/homeassistant/components/transmission/.translations/sl.json +++ b/homeassistant/components/transmission/.translations/sl.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Gostitelj je \u017ee konfiguriran.", "one_instance_allowed": "Potrebna je samo ena instanca." }, "error": { "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", + "name_exists": "Ime \u017ee obstaja", "wrong_credentials": "Napa\u010dno uporabni\u0161ko ime ali geslo" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Pogostost posodabljanja" }, - "description": "Nastavite mo\u017enosti za Transmission" + "description": "Nastavite mo\u017enosti za Transmission", + "title": "Nastavite mo\u017enosti za Transmission" } } } diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index 7f32ade8a08..2ee52b29b2d 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -7,6 +7,13 @@ "default": "Uspe\u0161no overjen z Withings za izbrani profil." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Kateri profil ste izbrali na spletni strani Withings? Pomembno je, da se profili ujemajo, sicer bodo podatki napa\u010dno ozna\u010deni.", + "title": "Uporabni\u0161ki profil." + }, "user": { "data": { "profile": "Profil" From 67eeb8f2582054ff5b65d361203f119ed66977eb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Nov 2019 21:21:13 -0700 Subject: [PATCH 131/306] Fix flaky test --- tests/test_requirements.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 782b4386552..2627a077a87 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -144,14 +144,18 @@ async def test_get_integration_with_requirements(hass): assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 3 - assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" - assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" - assert mock_is_installed.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" + assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] assert len(mock_inst.mock_calls) == 3 - assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" - assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" - assert mock_inst.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" + assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] async def test_install_with_wheels_index(hass): From 0d432f60e201a5ba650082df749c8c9c428de582 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sun, 3 Nov 2019 00:21:46 -0400 Subject: [PATCH 132/306] Bump env_canada to 0.0.30 (#28487) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 7661269073c..8ad13b39251 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,7 +3,7 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": [ - "env_canada==0.0.29" + "env_canada==0.0.30" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9723075b98b..a582478d132 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -462,7 +462,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.29 +env_canada==0.0.30 # homeassistant.components.envirophat # envirophat==0.0.6 From 31752d5736d512e9bfb0481f809e7c51de94d5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Rutkai?= Date: Sun, 3 Nov 2019 05:24:02 +0100 Subject: [PATCH 133/306] Fixing #27722 Watson TTS platform (sdk upgrade) (#28468) --- homeassistant/components/watson_tts/manifest.json | 2 +- homeassistant/components/watson_tts/tts.py | 5 ++++- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index 4cde3f764e5..e5f3efae04c 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -3,7 +3,7 @@ "name": "IBM Watson TTS", "documentation": "https://www.home-assistant.io/integrations/watson_tts", "requirements": [ - "ibm-watson==3.0.3" + "ibm-watson==4.0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index a30d08f31f3..021767e3d11 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -93,8 +93,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_engine(hass, config): """Set up IBM Watson TTS component.""" from ibm_watson import TextToSpeechV1 + from ibm_cloud_sdk_core.authenticators import IAMAuthenticator - service = TextToSpeechV1(url=config[CONF_URL], iam_apikey=config[CONF_APIKEY]) + authenticator = IAMAuthenticator(config[CONF_APIKEY]) + service = TextToSpeechV1(authenticator) + service.set_service_url(config[CONF_URL]) supported_languages = list({s[:5] for s in SUPPORTED_VOICES}) default_voice = config[CONF_VOICE] diff --git a/requirements_all.txt b/requirements_all.txt index a582478d132..cf2c0cb7918 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -685,7 +685,7 @@ hydrawiser==0.1.1 iaqualink==0.3.0 # homeassistant.components.watson_tts -ibm-watson==3.0.3 +ibm-watson==4.0.1 # homeassistant.components.watson_iot ibmiotf==0.3.4 From 5cbb6607a62ff7dcca3405b3b2ba1c7040ef01ac Mon Sep 17 00:00:00 2001 From: Tim McCormick Date: Sun, 3 Nov 2019 04:25:24 +0000 Subject: [PATCH 134/306] Fix missing import (#28460) --- homeassistant/components/sonos/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 94d252e9fee..2baa02d0a5d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -8,6 +8,7 @@ import urllib import async_timeout import pysonos +from pysonos import alarms from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.snapshot @@ -1163,7 +1164,7 @@ class SonosEntity(MediaPlayerDevice): """Set the alarm clock on the player.""" alarm = None - for one_alarm in pysonos.alarms.get_alarms(self.soco): + for one_alarm in alarms.get_alarms(self.soco): # pylint: disable=protected-access if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]): alarm = one_alarm From 314c3d0965134ca7aff639a0a56e2ed129519d4c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 3 Nov 2019 05:28:07 +0100 Subject: [PATCH 135/306] Use integration name in docstring (#28445) --- script/scaffold/templates/config_flow/integration/__init__.py | 4 ++-- .../templates/config_flow_discovery/integration/__init__.py | 4 ++-- .../templates/config_flow_oauth2/integration/__init__.py | 2 +- .../device_condition/integration/device_condition.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 403453a1f6b..04b908952d1 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -21,9 +21,9 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Somfy from a config entry.""" + """Set up NEW_NAME from a config entry.""" # TODO Store an API object for your platforms to access - # hass.data[DOMAIN][entry.entry_id] = MyApi(…) + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) for component in PLATFORMS: hass.async_create_task( diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index 403453a1f6b..04b908952d1 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -21,9 +21,9 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Somfy from a config entry.""" + """Set up NEW_NAME from a config entry.""" # TODO Store an API object for your platforms to access - # hass.data[DOMAIN][entry.entry_id] = MyApi(…) + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) for component in PLATFORMS: hass.async_create_task( diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index 43b4c6f31cd..30e7ad97810 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -55,7 +55,7 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Somfy from a config entry.""" + """Set up NEW_NAME from a config entry.""" implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry ) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index fa123cff8e0..4b7baf68a37 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,4 +1,4 @@ -"""Provides device automations for NEW_NAME.""" +"""Provide the device automations for NEW_NAME.""" from typing import Dict, List import voluptuous as vol From b904a2c5ada4f2ada097f139909418fb2b1b0217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 3 Nov 2019 06:28:38 +0200 Subject: [PATCH 136/306] Handle Huawei LTE timeouts (#28465) --- .../huawei_lte/.translations/en.json | 1 + .../components/huawei_lte/__init__.py | 14 +++++++++++--- .../components/huawei_lte/config_flow.py | 18 ++++++++++++++---- homeassistant/components/huawei_lte/const.py | 2 ++ .../components/huawei_lte/strings.json | 1 + 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 8681e3355a4..4e366e0ce31 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -5,6 +5,7 @@ }, "error": { "connection_failed": "Connection failed", + "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index e224f45ba90..112fcb6ec52 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -18,6 +18,7 @@ from huawei_lte_api.exceptions import ( ResponseErrorLoginRequiredException, ResponseErrorNotSupportedException, ) +from requests.exceptions import Timeout from url_normalize import url_normalize from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN @@ -33,6 +34,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CALLBACK_TYPE +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -44,6 +46,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType from .const import ( ALL_KEYS, + CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DOMAIN, KEY_DEVICE_BASIC_INFORMATION, @@ -254,16 +257,21 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) username = config_entry.data.get(CONF_USERNAME) password = config_entry.data.get(CONF_PASSWORD) if username or password: - connection = AuthorizedConnection(url, username=username, password=password) + connection = AuthorizedConnection( + url, username=username, password=password, timeout=CONNECTION_TIMEOUT + ) else: - connection = Connection(url) + connection = Connection(url, timeout=CONNECTION_TIMEOUT) return connection def signal_update() -> None: """Signal updates to data.""" dispatcher_send(hass, UPDATE_SIGNAL, url) - connection = await hass.async_add_executor_job(get_connection) + try: + connection = await hass.async_add_executor_job(get_connection) + except Timeout as ex: + raise ConfigEntryNotReady from ex # Set up router and store reference to it router = Router(connection, url, mac, signal_update) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 52d586d088a..992dc33a697 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -14,13 +14,14 @@ from huawei_lte_api.exceptions import ( LoginErrorUsernamePasswordOverrunException, ResponseErrorException, ) +from requests.exceptions import Timeout from url_normalize import url_normalize import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME from homeassistant.core import callback -from .const import DEFAULT_DEVICE_NAME +from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME # https://github.com/PyCQA/pylint/issues/3202 from .const import DOMAIN # pylint: disable=unused-import @@ -115,12 +116,18 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Try connecting with given credentials.""" if username or password: conn = AuthorizedConnection( - user_input[CONF_URL], username=username, password=password + user_input[CONF_URL], + username=username, + password=password, + timeout=CONNECTION_TIMEOUT, ) else: try: conn = AuthorizedConnection( - user_input[CONF_URL], username="", password="" + user_input[CONF_URL], + username="", + password="", + timeout=CONNECTION_TIMEOUT, ) user_input[CONF_USERNAME] = "" user_input[CONF_PASSWORD] = "" @@ -129,7 +136,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "Could not login with empty credentials, proceeding unauthenticated", exc_info=True, ) - conn = Connection(user_input[CONF_URL]) + conn = Connection(user_input[CONF_URL], timeout=CONNECTION_TIMEOUT) del user_input[CONF_USERNAME] del user_input[CONF_PASSWORD] return conn @@ -170,6 +177,9 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except ResponseErrorException: _LOGGER.warning("Response error", exc_info=True) errors["base"] = "response_error" + except Timeout: + _LOGGER.warning("Connection timeout", exc_info=True) + errors[CONF_URL] = "connection_timeout" except Exception: # pylint: disable=broad-except _LOGGER.warning("Unknown error connecting to device", exc_info=True) errors[CONF_URL] = "unknown_connection_error" diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 3d99261e6ed..8dae63f6538 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -10,6 +10,8 @@ UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" UNIT_BYTES = "B" UNIT_SECONDS = "s" +CONNECTION_TIMEOUT = 10 + KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 8681e3355a4..2e76cf1b343 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -11,6 +11,7 @@ "invalid_url": "Invalid URL", "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", "response_error": "Unknown error from device", + "connection_timeout": "Connection timeout", "unknown_connection_error": "Unknown error connecting to device" }, "step": { From ecf2e9c0ab781b1b5b2af68905bf043fea3f3ea2 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 3 Nov 2019 18:32:01 +0100 Subject: [PATCH 137/306] Fix flaky Samsung TV tests (#28503) --- tests/components/samsungtv/test_media_player.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index c178710e3f9..deb39b4077f 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -2,6 +2,7 @@ import asyncio from asynctest import mock from datetime import timedelta +import logging import pytest from samsungctl import exceptions from tests.common import MockDependency, async_fire_time_changed @@ -263,6 +264,7 @@ async def test_send_key_autodetect_websocket(hass, remote): async def test_send_key_autodetect_websocket_exception(hass, caplog): """Test for send key with autodetection of protocol.""" + caplog.set_level(logging.DEBUG) with patch( "samsungctl.Remote", side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT] ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): @@ -458,6 +460,7 @@ async def test_turn_off_legacy(hass, remote): async def test_turn_off_os_error(hass, remote, caplog): """Test for turn_off with OSError.""" + caplog.set_level(logging.DEBUG) await setup_samsungtv(hass, MOCK_CONFIG) remote.close = mock.Mock(side_effect=OSError("BOOM")) assert await hass.services.async_call( From 5fd9b474dca3c9eaca6ad6f86625cc54ba88175f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 3 Nov 2019 20:36:02 +0100 Subject: [PATCH 138/306] Always provide brightness value (#28228) HA will remove attribute when light is off, but google expect all trait data all the time. --- homeassistant/components/google_assistant/trait.py | 2 ++ tests/components/google_assistant/test_smart_home.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 7d6e79a8237..6b5530ab2ce 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -185,6 +185,8 @@ class BrightnessTrait(_Trait): brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS) if brightness is not None: response["brightness"] = int(100 * (brightness / 255)) + else: + response["brightness"] = 0 return response diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 2f7fdb8e131..3c3801b35c6 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -226,7 +226,7 @@ async def test_query_message(hass): "payload": { "devices": { "light.non_existing": {"online": False}, - "light.demo_light": {"on": False, "online": True}, + "light.demo_light": {"on": False, "online": True, "brightness": 0}, "light.another_light": { "on": True, "online": True, From 0768ae2dc80e584c8f14a930248f97646315de37 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 3 Nov 2019 23:56:08 +0100 Subject: [PATCH 139/306] Fix flaky YesssSMS tests on debug messages (#28506) --- tests/components/yessssms/test_notify.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index 5cc204ccc4d..dbc33b5a388 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import patch +import logging import pytest import requests_mock @@ -101,6 +102,7 @@ async def test_false_login_data_error(hass, caplog, valid_settings, invalid_logi async def test_init_success(hass, caplog, valid_settings, valid_login_data): """Test for successful init of yessssms.""" + caplog.set_level(logging.DEBUG) await valid_settings assert hass.services.has_service("notify", "sms") messages = [] @@ -119,6 +121,7 @@ async def test_init_success(hass, caplog, valid_settings, valid_login_data): async def test_connection_error_on_init(hass, caplog, valid_settings, connection_error): """Test for connection error on init.""" + caplog.set_level(logging.DEBUG) await valid_settings assert hass.services.has_service("notify", "sms") for record in caplog.records: From 734e98282268cbd21b5eee7013181ae77d535e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Nov 2019 01:05:01 +0200 Subject: [PATCH 140/306] Import CancelledError from asyncio, not .futures (#28511) It's no longer in .futures in Python 3.8.0. --- homeassistant/components/bluesound/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 702cf5ddc30..762f231b341 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -1,6 +1,6 @@ """Support for Bluesound devices.""" import asyncio -from asyncio.futures import CancelledError +from asyncio import CancelledError from datetime import timedelta import logging from urllib import parse From 9b038bd10d5945a33ce2ad7079bd71a613a2d658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Nov 2019 01:24:24 +0200 Subject: [PATCH 141/306] Don't use deprecated encoding to json.loads (#28509) Will be removed in 3.9, ignored in earlier supported versions. --- tests/components/homematicip_cloud/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 78c78ec0ab9..494ec2dc90b 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -92,7 +92,7 @@ class HomeTemplate(Home): def init_home(self, json_path=HOME_JSON): """Init template with json.""" - self.init_json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") + self.init_json_state = json.loads(load_fixture(HOME_JSON)) self.update_home(json_state=self.init_json_state, clearConfig=True) return self From 4e40394972fb35da8ba89c74e1ee4b00afa8c2e4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 4 Nov 2019 00:58:35 +0100 Subject: [PATCH 142/306] Fix Airly if more than one config entry (#28498) --- homeassistant/components/airly/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 80f3518c652..ce165918ac2 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -31,6 +31,8 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up configured Airly.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} return True @@ -49,8 +51,6 @@ async def async_setup_entry(hass, config_entry): if not airly.data: raise ConfigEntryNotReady() - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly hass.async_create_task( From 0973f1961d07bf7a1258271c5ec636231256c972 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 4 Nov 2019 00:32:38 +0000 Subject: [PATCH 143/306] [ci skip] Translation update --- homeassistant/components/huawei_lte/.translations/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 4e366e0ce31..8681e3355a4 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -5,7 +5,6 @@ }, "error": { "connection_failed": "Connection failed", - "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", From cb2f42b3367ca44af5dc5181a3f3d3569ad2d42d Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Sun, 3 Nov 2019 22:11:14 -0500 Subject: [PATCH 144/306] Update Vivotek component stream source (#27941) * Update Vivotek component Fix building stream URL * Update Vivotek component Make stream path optionally configurable. * Update Vivotek camera integration Use f-string to build stream source URL. This improve readability and I hear it runs faster. --- homeassistant/components/vivotek/camera.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 012c1e1df34..c39a9b495bd 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -20,9 +20,12 @@ _LOGGER = logging.getLogger(__name__) CONF_FRAMERATE = "framerate" +CONF_STREAM_PATH = "stream_path" + DEFAULT_CAMERA_BRAND = "Vivotek" DEFAULT_NAME = "Vivotek Camera" DEFAULT_EVENT_0_KEY = "event_i0_enable" +DEFAULT_STREAM_SOURCE = "live.sdp" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -33,12 +36,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + vol.Optional(CONF_STREAM_PATH, default=DEFAULT_STREAM_SOURCE): cv.string, } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Vivotek IP Camera.""" + creds = f"{config[CONF_USERNAME]}:{config[CONF_PASSWORD]}" args = dict( config=config, cam=VivotekCamera( @@ -48,12 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): usr=config[CONF_USERNAME], pwd=config[CONF_PASSWORD], ), - stream_source=( - "rtsp://%s:%s@%s:554/live.sdp", - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_IP_ADDRESS], - ), + stream_source=f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}", ) add_entities([VivotekCam(**args)], True) From 7e6bcb85b73a079f3447bc42efc97a95288c1782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Nov 2019 09:12:15 +0200 Subject: [PATCH 145/306] Don't fail tox pylint if PYLINT_ARGS is not set (#28403) Closes https://github.com/home-assistant/home-assistant/issues/28342 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 118d0722b7c..2898c410b2a 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pylint {env:PYLINT_ARGS} {posargs} homeassistant + pylint {env:PYLINT_ARGS:} {posargs} homeassistant [testenv:lint] deps = From c01b7bbf283fa633e94f07498ce6b1f95250f099 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:03:58 +0100 Subject: [PATCH 146/306] Upgrade pillow to 6.2.1 (#28442) --- homeassistant/components/image_processing/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 6a88a358f1d..e986ac6f4ca 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,7 +3,7 @@ "name": "Image processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "requirements": [ - "pillow==6.2.0" + "pillow==6.2.1" ], "dependencies": [ "camera" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index e3f62514801..39f7b9064fc 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,7 +3,7 @@ "name": "Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", "requirements": [ - "pillow==6.2.0" + "pillow==6.2.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index a3130070cc3..1f8caa3753a 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -3,7 +3,7 @@ "name": "Qrcode", "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": [ - "pillow==6.2.0", + "pillow==6.2.1", "pyzbar==0.1.7" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index cf2c0cb7918..38770a24686 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.2.0 +pillow==6.2.1 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7b4e4ffb01..89a86a8ce2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.2.0 +pillow==6.2.1 # homeassistant.components.plex plexapi==3.0.6 From 7cbd55a8179b54a1559f5c6a21e66bfd365d8918 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 4 Nov 2019 09:55:12 +0100 Subject: [PATCH 147/306] Add dump config service to HomematicIP Cloud (#28231) * Add dump config service to HomematicIP Cloud * Mock builin.open * Fix test * reduce SGTIN if anonymize * apply review feedback --- .../components/homematicip_cloud/__init__.py | 50 ++++++++++++++++++- .../homematicip_cloud/services.yaml | 13 ++++- .../components/homematicip_cloud/test_init.py | 15 +++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 9a0eb65aa3f..d6ae05c463a 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,7 +1,9 @@ """Support for HomematicIP Cloud devices.""" import logging +from pathlib import Path from homematicip.aio.group import AsyncHeatingGroup +from homematicip.base.helpers import handle_config import voluptuous as vol from homeassistant import config_entries @@ -26,17 +28,23 @@ from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 _LOGGER = logging.getLogger(__name__) +ATTR_ACCESSPOINT_ID = "accesspoint_id" +ATTR_ANONYMIZE = "anonymize" ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" +ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix" +ATTR_CONFIG_OUTPUT_PATH = "config_output_path" ATTR_DURATION = "duration" ATTR_ENDTIME = "endtime" ATTR_TEMPERATURE = "temperature" -ATTR_ACCESSPOINT_ID = "accesspoint_id" + +DEFAULT_CONFIG_FILE_PREFIX = "hmip-config" SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration" SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period" SERVICE_ACTIVATE_VACATION = "activate_vacation" SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" +SERVICE_DUMP_HAP_CONFIG = "dump_hap_config" SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" CONFIG_SCHEMA = vol.Schema( @@ -96,6 +104,16 @@ SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema( } ) +SCHEMA_DUMP_HAP_CONFIG = vol.Schema( + { + vol.Optional(ATTR_CONFIG_OUTPUT_PATH): cv.string, + vol.Optional( + ATTR_CONFIG_OUTPUT_FILE_PREFIX, default=DEFAULT_CONFIG_FILE_PREFIX + ): cv.string, + vol.Optional(ATTR_ANONYMIZE, default=True): cv.boolean, + } +) + async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -239,6 +257,36 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, ) + async def _async_dump_hap_config(service): + """Service to dump the configuration of a Homematic IP Access Point.""" + config_path = ( + service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir + ) + config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] + anonymize = service.data[ATTR_ANONYMIZE] + + for hap in hass.data[DOMAIN].values(): + hap_sgtin = hap.config_entry.title + + if anonymize: + hap_sgtin = hap_sgtin[-4:] + + file_name = f"{config_file_prefix}_{hap_sgtin}.json" + path = Path(config_path) + config_file = path / file_name + + json_state = await hap.home.download_configuration() + json_state = handle_config(json_state, anonymize) + + config_file.write_text(json_state, encoding="utf8") + + hass.services.async_register( + DOMAIN, + SERVICE_DUMP_HAP_CONFIG, + _async_dump_hap_config, + schema=SCHEMA_DUMP_HAP_CONFIG, + ) + def _get_home(hapid: str): """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index f426c9b5d22..9a7d90eba9c 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -57,4 +57,15 @@ set_active_climate_profile: description: The index of the climate profile (1 based) example: 1 - +dump_hap_config: + description: Dump the configuration of the Homematic IP Access Point(s). + fields: + config_output_path: + description: (Default is 'Your home-assistant config directory') Path where to store the config. + example: '/config' + config_output_file_prefix: + description: (Default is 'hmip-config') Name of the config file. The SGTIN of the AP will always be appended. + example: 'hmip-config' + anonymize: + description: (Default is True) Should the Configuration be anonymized? + example: True diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 894db2e691b..ba27a619e6a 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import patch from homeassistant.components import homematicip_cloud as hmipc from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.common import Mock, MockConfigEntry, mock_coro async def test_config_with_accesspoint_passed_to_config_entry(hass): @@ -150,3 +150,16 @@ async def test_unload_entry(hass): assert await hmipc.async_unload_entry(hass, entry) assert len(mock_hap.return_value.async_reset.mock_calls) == 1 assert hass.data[hmipc.DOMAIN] == {} + + +async def test_hmip_dump_hap_config_services(hass, mock_hap_with_service): + """Test dump configuration services.""" + + with patch("pathlib.Path.write_text", return_value=Mock()) as write_mock: + await hass.services.async_call( + "homematicip_cloud", "dump_hap_config", {"anonymize": True}, blocking=True + ) + home = mock_hap_with_service.home + assert home.mock_calls[-1][0] == "download_configuration" + assert len(home.mock_calls) == 8 # pylint: disable=W0212 + assert len(write_mock.mock_calls) > 0 From 381bf987d24f885166ef20f10e0b8db0214b34ce Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:56:27 +0100 Subject: [PATCH 148/306] Upgrade TwitterAPI to 2.5.10 (#28401) --- homeassistant/components/twitter/manifest.json | 2 +- homeassistant/components/twitter/notify.py | 3 +-- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index 2713004343b..d63259bcbee 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -3,7 +3,7 @@ "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", "requirements": [ - "TwitterAPI==2.5.9" + "TwitterAPI==2.5.10" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 2991bea8d2e..39faf987ae0 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -6,6 +6,7 @@ import logging import mimetypes import os +from TwitterAPI import TwitterAPI import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME @@ -62,8 +63,6 @@ class TwitterNotificationService(BaseNotificationService): username, ): """Initialize the service.""" - from TwitterAPI import TwitterAPI - self.user = username self.hass = hass self.api = TwitterAPI( diff --git a/requirements_all.txt b/requirements_all.txt index 38770a24686..d7a8e1be3b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -94,7 +94,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.5.9 +TwitterAPI==2.5.10 # homeassistant.components.tof # VL53L1X2==0.1.5 From e91bb1ab087709954e8c5e880810508070ecc802 Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Mon, 4 Nov 2019 00:56:36 -0800 Subject: [PATCH 149/306] Replace Netatmo CO2 sensor icon (#28520) --- homeassistant/components/netatmo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 70b6297388c..3d4b5f2fa51 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -62,7 +62,7 @@ SENSOR_TYPES = { "mdi:thermometer", DEVICE_CLASS_TEMPERATURE, ], - "co2": ["CO2", "ppm", "mdi:cloud", None], + "co2": ["CO2", "ppm", "mdi:periodic-table-co2", None], "pressure": ["Pressure", "mbar", "mdi:gauge", None], "noise": ["Noise", "dB", "mdi:volume-high", None], "humidity": ["Humidity", "%", "mdi:water-percent", DEVICE_CLASS_HUMIDITY], From fe00f3558e4d95d087bd97fec15c6ad37c48299d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:56:46 +0100 Subject: [PATCH 150/306] Imports twitch (#28517) * Move imports * Add unique_id --- homeassistant/components/twitch/sensor.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index b0051e1c765..f4276160d6c 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -1,11 +1,13 @@ """Support for the Twitch stream status.""" import logging +from requests.exceptions import HTTPError +from twitch import TwitchClient import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -14,6 +16,7 @@ ATTR_TITLE = "title" CONF_CHANNELS = "channels" CONF_CLIENT_ID = "client_id" + ICON = "mdi:twitch" STATE_OFFLINE = "offline" @@ -22,18 +25,16 @@ STATE_STREAMING = "streaming" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CHANNELS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]), } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Twitch platform.""" - from twitch import TwitchClient - from requests.exceptions import HTTPError - - channels = config.get(CONF_CHANNELS, []) - client = TwitchClient(client_id=config.get(CONF_CLIENT_ID)) + channels = config[CONF_CHANNELS] + client_id = config[CONF_CLIENT_ID] + client = TwitchClient(client_id=client_id) try: client.ingests.get_server_list() @@ -55,8 +56,7 @@ class TwitchSensor(Entity): self._user = user self._channel = self._user.name self._id = self._user.id - self._state = STATE_OFFLINE - self._preview = self._game = self._title = None + self._state = self._preview = self._game = self._title = None @property def should_poll(self): @@ -84,6 +84,11 @@ class TwitchSensor(Entity): if self._state == STATE_STREAMING: return {ATTR_GAME: self._game, ATTR_TITLE: self._title} + @property + def unique_id(self): + """Return unique ID for this sensor.""" + return self._id + @property def icon(self): """Icon to use in the frontend, if any.""" From cb19827932219556bda69902136e6048073ece7e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:56:58 +0100 Subject: [PATCH 151/306] Upgrade paho-mqtt to 1.5.0 (#28423) --- homeassistant/components/mqtt/manifest.json | 2 +- homeassistant/components/shiftr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index c1b6659e1c0..b8ec38ec100 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": [ "hbmqtt==0.9.5", - "paho-mqtt==1.4.0" + "paho-mqtt==1.5.0" ], "dependencies": [ "http" diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index 282d86ce419..b228909a59e 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -3,7 +3,7 @@ "name": "Shiftr", "documentation": "https://www.home-assistant.io/integrations/shiftr", "requirements": [ - "paho-mqtt==1.4.0" + "paho-mqtt==1.5.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d7a8e1be3b7..07095414b0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -933,7 +933,7 @@ orvibo==1.1.1 # homeassistant.components.mqtt # homeassistant.components.shiftr -paho-mqtt==1.4.0 +paho-mqtt==1.5.0 # homeassistant.components.panasonic_bluray panacotta==0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89a86a8ce2f..6499ac04681 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -324,7 +324,7 @@ oauth2client==4.0.0 # homeassistant.components.mqtt # homeassistant.components.shiftr -paho-mqtt==1.4.0 +paho-mqtt==1.5.0 # homeassistant.components.aruba # homeassistant.components.cisco_ios From 60d7f730c316639f9a04b239a4dbb2ca9e769312 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:57:20 +0100 Subject: [PATCH 152/306] Upgrade jinja2 to >=2.10.3 (#28422) --- 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 03e43ba139a..7a7d8331952 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ distro==1.4.0 hass-nabucasa==0.23 home-assistant-frontend==20191025.1 importlib-metadata==0.23 -jinja2>=2.10.1 +jinja2>=2.10.3 netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 07095414b0b..b831ec7af89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -7,7 +7,7 @@ bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 -jinja2>=2.10.1 +jinja2>=2.10.3 PyJWT==1.7.1 cryptography==2.8 pip>=8.0.3 diff --git a/setup.py b/setup.py index 612ac7d0ce8..99195ee2f56 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ REQUIRES = [ "certifi>=2019.9.11", 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.23", - "jinja2>=2.10.1", + "jinja2>=2.10.3", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==2.8", From de1799d486bf0c55c14aa8672f6e0c84d454443b Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Mon, 4 Nov 2019 00:58:27 -0800 Subject: [PATCH 153/306] iaqualink: better handling of failures (#28514) --- .../components/iaqualink/__init__.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index c3fa2bb1eb8..fc1eb3b248a 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -4,6 +4,7 @@ from functools import wraps import logging from typing import Any, Dict +import aiohttp.client_exceptions import voluptuous as vol from iaqualink import ( @@ -26,6 +27,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -90,8 +92,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None try: await aqualink.login() except AqualinkLoginException as login_exception: - _LOGGER.error("Exception raised while attempting to login: %s", login_exception) + _LOGGER.error("Failed to login: %s", login_exception) return False + except ( + asyncio.TimeoutError, + aiohttp.client_exceptions.ClientConnectorError, + ) as aio_exception: + _LOGGER.warning("Exception raised while attempting to login: %s", aio_exception) + raise ConfigEntryNotReady systems = await aqualink.get_systems() systems = list(systems.values()) @@ -133,7 +141,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None async def _async_systems_update(now): """Refresh internal state for all systems.""" + prev = systems[0].last_run_success + await systems[0].update() + success = systems[0].last_run_success + + if not success and prev: + _LOGGER.warning("Failed to refresh iAqualink state") + elif success and not prev: + _LOGGER.warning("Reconnected to iAqualink") + async_dispatcher_send(hass, DOMAIN) async_track_time_interval(hass, _async_systems_update, UPDATE_INTERVAL) From 40b676c06f0d97504e8a18f4646e39acbeb266bf Mon Sep 17 00:00:00 2001 From: Tobias Efinger Date: Mon, 4 Nov 2019 10:29:29 +0100 Subject: [PATCH 154/306] Add services description for ness alarm (#28250) --- .../components/ness_alarm/services.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/ness_alarm/services.yaml b/homeassistant/components/ness_alarm/services.yaml index e69de29bb2d..27f8ba383dd 100644 --- a/homeassistant/components/ness_alarm/services.yaml +++ b/homeassistant/components/ness_alarm/services.yaml @@ -0,0 +1,19 @@ +# Describes the format for available ness alarm services + +aux: + description: Trigger an aux output. + fields: + output_id: + description: The aux output you wish to change. A number from 1-4. + example: 1 + state: + description: The On/Off State, represented as true/false. Default is true. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E. + example: true + default: true + +panic: + description: Trigger a panic + fields: + code: + description: The user code to use to trigger the panic. + example: 1234 \ No newline at end of file From fadb9bdfb371cdd42e82f0cbd71be48ae27a1964 Mon Sep 17 00:00:00 2001 From: Jonas Janz Date: Mon, 4 Nov 2019 10:58:39 +0100 Subject: [PATCH 155/306] Add information to IFTTT services.yaml (#28385) * docs(ifttt): add information to services.yaml * docs(ifttt): start examples lowercase * docs(ifttt): start examples with capital letter * docs(ifttt): end description on period --- homeassistant/components/ifttt/services.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml index e69de29bb2d..8669bc07fb4 100644 --- a/homeassistant/components/ifttt/services.yaml +++ b/homeassistant/components/ifttt/services.yaml @@ -0,0 +1,18 @@ +# Describes the format for available ifttt services + + +trigger: + description: Triggers the configured IFTTT Webhook. + fields: + event: + description: The name of the event to sent. + example: 'MY_HA_EVENT' + value1: + description: Generic field to send data via the event. + example: 'Hello World' + value2: + description: Generic field to send data via the event. + example: 'some additional data' + value3: + description: Generic field to send data via the event. + example: 'even more data' \ No newline at end of file From 6004ef3279ce5ce0dc29ed5a049196bf0f7c21df Mon Sep 17 00:00:00 2001 From: Oscar Tin Lai Date: Mon, 4 Nov 2019 21:10:59 +1100 Subject: [PATCH 156/306] Expose set auto mode for all Dyson fans (#28488) Set auto mode should be exposed to all dyson fans (e.g. *Pure Cool Link* and *Pure Hot+Cool Link*) instead of only *Pure Cool*, as it is support in all of the models (i.e. similar to the set night mode). --- homeassistant/components/dyson/fan.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 0165044b839..341919935d0 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -151,14 +151,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): service_handle, schema=DYSON_SET_NIGHT_MODE_SCHEMA, ) - if has_purecool_devices: - hass.services.register( - DYSON_DOMAIN, - SERVICE_SET_AUTO_MODE, - service_handle, - schema=SET_AUTO_MODE_SCHEMA, - ) + hass.services.register( + DYSON_DOMAIN, + SERVICE_SET_AUTO_MODE, + service_handle, + schema=SET_AUTO_MODE_SCHEMA, + ) + if has_purecool_devices: hass.services.register( DYSON_DOMAIN, SERVICE_SET_ANGLE, service_handle, schema=SET_ANGLE_SCHEMA ) From 9b72a55d60c5ac07c494d2bde17a79e75b348773 Mon Sep 17 00:00:00 2001 From: Luca Zimmermann Date: Mon, 4 Nov 2019 11:11:10 +0100 Subject: [PATCH 157/306] Add compatibility for other STBY Codes (#28478) Added PWR2 as valid standby code --- homeassistant/components/pioneer/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 0c6de9803e1..51f55d4e851 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -169,6 +169,8 @@ class PioneerDevice(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" + if self._pwstate == "PWR2": + return STATE_OFF if self._pwstate == "PWR1": return STATE_OFF if self._pwstate == "PWR0": From 552fbda58b3cd66104a780712ebb6ac1233630de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Nov 2019 02:12:04 -0800 Subject: [PATCH 158/306] Remove legacy reproduce state (#28458) * Remove legacy reproduce state * Fix imports --- .../components/homeassistant/scene.py | 8 +- homeassistant/components/scene/__init__.py | 3 +- homeassistant/helpers/state.py | 144 +----------------- 3 files changed, 12 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 45560d30edb..084c950bf17 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -14,7 +14,7 @@ from homeassistant.const import ( STATE_ON, SERVICE_RELOAD, ) -from homeassistant.core import State, DOMAIN +from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant import config as conf_util from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_get_integration @@ -23,7 +23,7 @@ from homeassistant.helpers import ( config_validation as cv, entity_platform, ) -from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state +from homeassistant.helpers.state import async_reproduce_state from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene @@ -60,7 +60,7 @@ STATES_SCHEMA = vol.All(dict, _convert_states) PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): HASS_DOMAIN, + vol.Required(CONF_PLATFORM): HA_DOMAIN, vol.Required(STATES): vol.All( cv.ensure_list, [ @@ -114,7 +114,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Extract only the config for the Home Assistant platform, ignore the rest. for p_type, p_config in config_per_platform(conf, SCENE_DOMAIN): - if p_type != DOMAIN: + if p_type != HA_DOMAIN: continue _process_scenes_config(hass, async_add_entities, p_config) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index ec2dc3118a9..63a64f34fe9 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -8,7 +8,6 @@ from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.state import HASS_DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs @@ -21,7 +20,7 @@ STATES = "states" def _hass_domain_validator(config): """Validate platform in config for homeassistant domain.""" if CONF_PLATFORM not in config: - config = {CONF_PLATFORM: HASS_DOMAIN, STATES: config} + config = {CONF_PLATFORM: HA_DOMAIN, STATES: config} return config diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 4cb7fb85bff..abc97bf1f8a 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,35 +1,15 @@ """Helpers that help with state related things.""" import asyncio import datetime as dt -import json import logging from collections import defaultdict from types import ModuleType, TracebackType -from typing import Awaitable, Dict, Iterable, List, Optional, Tuple, Type, Union +from typing import Dict, Iterable, List, Optional, Type, Union from homeassistant.loader import bind_hass, async_get_integration, IntegrationNotFound import homeassistant.util.dt as dt_util -from homeassistant.components.notify import ATTR_MESSAGE, SERVICE_NOTIFY from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON -from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.const import ( - ATTR_ENTITY_ID, - SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_HOME, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_TRIGGER, - SERVICE_LOCK, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - SERVICE_UNLOCK, - SERVICE_OPEN_COVER, - SERVICE_CLOSE_COVER, - SERVICE_SET_COVER_POSITION, - SERVICE_SET_COVER_TILT_POSITION, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_HOME, STATE_LOCKED, @@ -40,36 +20,11 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNLOCKED, ) -from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN +from homeassistant.core import Context, State from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) -GROUP_DOMAIN = "group" - -# Update this dict of lists when new services are added to HA. -# Each item is a service with a list of required attributes. -SERVICE_ATTRIBUTES = { - SERVICE_NOTIFY: [ATTR_MESSAGE], - SERVICE_SET_COVER_POSITION: [ATTR_POSITION], - SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION], -} - -# Update this dict when new services are added to HA. -# Each item is a service with a corresponding state. -SERVICE_TO_STATE = { - SERVICE_TURN_ON: STATE_ON, - SERVICE_TURN_OFF: STATE_OFF, - SERVICE_ALARM_ARM_AWAY: STATE_ALARM_ARMED_AWAY, - SERVICE_ALARM_ARM_HOME: STATE_ALARM_ARMED_HOME, - SERVICE_ALARM_DISARM: STATE_ALARM_DISARMED, - SERVICE_ALARM_TRIGGER: STATE_ALARM_TRIGGERED, - SERVICE_LOCK: STATE_LOCKED, - SERVICE_UNLOCK: STATE_UNLOCKED, - SERVICE_OPEN_COVER: STATE_OPEN, - SERVICE_CLOSE_COVER: STATE_CLOSED, -} - class AsyncTrackStates: """ @@ -109,18 +64,6 @@ def get_changed_since( return [state for state in states if state.last_updated >= utc_point_in_time] -@bind_hass -def reproduce_state( - hass: HomeAssistantType, - states: Union[State, Iterable[State]], - blocking: bool = False, -) -> None: - """Reproduce given state.""" - return asyncio.run_coroutine_threadsafe( - async_reproduce_state(hass, states, blocking), hass.loop - ).result() - - @bind_hass async def async_reproduce_state( hass: HomeAssistantType, @@ -149,16 +92,12 @@ async def async_reproduce_state( try: platform: Optional[ModuleType] = integration.get_platform("reproduce_state") except ImportError: - platform = None + _LOGGER.warning("Integration %s does not support reproduce state", domain) + return - if platform: - await platform.async_reproduce_states( # type: ignore - hass, states_by_domain, context=context - ) - else: - await async_reproduce_state_legacy( - hass, domain, states_by_domain, blocking=blocking, context=context - ) + await platform.async_reproduce_states( # type: ignore + hass, states_by_domain, context=context + ) if to_call: # run all domains in parallel @@ -167,75 +106,6 @@ async def async_reproduce_state( ) -@bind_hass -async def async_reproduce_state_legacy( - hass: HomeAssistantType, - domain: str, - states: Iterable[State], - blocking: bool = False, - context: Optional[Context] = None, -) -> None: - """Reproduce given state.""" - to_call: Dict[Tuple[str, str], List[str]] = defaultdict(list) - - if domain == GROUP_DOMAIN: - service_domain = HASS_DOMAIN - else: - service_domain = domain - - for state in states: - - if hass.states.get(state.entity_id) is None: - _LOGGER.warning( - "reproduce_state: Unable to find entity %s", state.entity_id - ) - continue - - domain_services = hass.services.async_services().get(service_domain) - - if not domain_services: - _LOGGER.warning("reproduce_state: Unable to reproduce state %s (1)", state) - continue - - service = None - for _service in domain_services.keys(): - if ( - _service in SERVICE_ATTRIBUTES - and all( - attr in state.attributes for attr in SERVICE_ATTRIBUTES[_service] - ) - or _service in SERVICE_TO_STATE - and SERVICE_TO_STATE[_service] == state.state - ): - service = _service - if ( - _service in SERVICE_TO_STATE - and SERVICE_TO_STATE[_service] == state.state - ): - break - - if not service: - _LOGGER.warning("reproduce_state: Unable to reproduce state %s (2)", state) - continue - - # We group service calls for entities by service call - # json used to create a hashable version of dict with maybe lists in it - key = (service, json.dumps(dict(state.attributes), sort_keys=True)) - to_call[key].append(state.entity_id) - - domain_tasks: List[Awaitable[Optional[bool]]] = [] - for (service, service_data), entity_ids in to_call.items(): - data = json.loads(service_data) - data[ATTR_ENTITY_ID] = entity_ids - - domain_tasks.append( - hass.services.async_call(service_domain, service, data, blocking, context) - ) - - if domain_tasks: - await asyncio.wait(domain_tasks) - - def state_as_number(state: State) -> float: """ Try to coerce our state to a number. From b7296c61bc1f91314af00ce52a5b376657fb9095 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 4 Nov 2019 12:05:39 +0100 Subject: [PATCH 159/306] Align attribute naming between light and switch for HomematicIP Cloud (#28271) --- homeassistant/components/homematicip_cloud/light.py | 12 ++++++------ tests/components/homematicip_cloud/test_light.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 46a8d95729f..044140e5582 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -28,8 +28,8 @@ from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) -ATTR_ENERGY_COUNTER = "energy_counter_kwh" -ATTR_POWER_CONSUMPTION = "power_consumption" +ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" +ATTR_CURRENT_POWER_W = "current_power_w" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -95,11 +95,11 @@ class HomematicipLightMeasuring(HomematicipLight): """Return the state attributes of the generic device.""" state_attr = super().device_state_attributes - current_power_consumption = self._device.currentPowerConsumption - if current_power_consumption > 0.05: - state_attr[ATTR_POWER_CONSUMPTION] = round(current_power_consumption, 2) + current_power_w = self._device.currentPowerConsumption + if current_power_w > 0.05: + state_attr[ATTR_CURRENT_POWER_W] = round(current_power_w, 2) - state_attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2) + state_attr[ATTR_TODAY_ENERGY_KWH] = round(self._device.energyCounter, 2) return state_attr diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 17e92d9d99d..a55d9ea9151 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -3,8 +3,8 @@ from homematicip.base.enums import RGBColorState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.light import ( - ATTR_ENERGY_COUNTER, - ATTR_POWER_CONSUMPTION, + ATTR_CURRENT_POWER_W, + ATTR_TODAY_ENERGY_KWH, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -209,8 +209,8 @@ async def test_hmip_light_measuring(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_POWER_CONSUMPTION] == 50 - assert ha_state.attributes[ATTR_ENERGY_COUNTER] == 6.33 + assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 + assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 6.33 await hass.services.async_call( "light", "turn_off", {"entity_id": entity_id}, blocking=True From 33c8cba30d76f66411642fe2f29cc81f5d9b2bc2 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 4 Nov 2019 12:22:28 +0100 Subject: [PATCH 160/306] Enable transition time for HmIP-BSL - HomematicIP Cloud (#28201) * Enable transition time for HmIP-BSL - HomematicIP Cloud harden ACP fix hao device name * update test, initalize instance var --- .../components/homematicip_cloud/__init__.py | 2 +- .../homematicip_cloud/alarm_control_panel.py | 12 ++++-- .../components/homematicip_cloud/light.py | 20 ++++++++- .../homematicip_cloud/test_light.py | 43 +++++++++++++------ 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index d6ae05c463a..8cd41e0b980 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -312,7 +312,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool device_registry = await dr.async_get_registry(hass) home = hap.home # Add the HAP name from configuration if set. - hapname = home.label if not home.name else f"{home.label} {home.name}" + hapname = home.label if not home.name else f"{home.name} {home.label}" device_registry.async_get_or_create( config_entry_id=home.id, identifiers={(DOMAIN, home.id)}, diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index f61bf6f6b56..a7b1beaec93 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -52,11 +52,13 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): """Initialize the alarm control panel.""" self._home = hap.home self.alarm_state = STATE_ALARM_DISARMED + self._internal_alarm_zone = None + self._external_alarm_zone = None for security_zone in security_zones: if security_zone.label == "INTERNAL": self._internal_alarm_zone = security_zone - else: + elif security_zone.label == "EXTERNAL": self._external_alarm_zone = security_zone @property @@ -110,8 +112,10 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): async def async_added_to_hass(self): """Register callbacks.""" - self._internal_alarm_zone.on_update(self._async_device_changed) - self._external_alarm_zone.on_update(self._async_device_changed) + if self._internal_alarm_zone: + self._internal_alarm_zone.on_update(self._async_device_changed) + if self._external_alarm_zone: + self._external_alarm_zone.on_update(self._async_device_changed) def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" @@ -146,7 +150,7 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): def _get_zone_alarm_state(security_zone) -> bool: - if security_zone.active: + if security_zone and security_zone.active: if ( security_zone.sabotage or security_zone.motionDetected diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 044140e5582..c262b05d019 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -16,6 +16,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_HS_COLOR, + ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, @@ -225,13 +226,28 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): # Minimum brightness is 10, otherwise the led is disabled brightness = max(10, brightness) dim_level = brightness / 255.0 + transition = kwargs.get(ATTR_TRANSITION, 0.5) - await self._device.set_rgb_dim_level(self.channel, simple_rgb_color, dim_level) + await self._device.set_rgb_dim_level_with_time( + channelIndex=self.channel, + rgb=simple_rgb_color, + dimLevel=dim_level, + onTime=0, + rampTime=transition, + ) async def async_turn_off(self, **kwargs): """Turn the light off.""" simple_rgb_color = self._func_channel.simpleRGBColorState - await self._device.set_rgb_dim_level(self.channel, simple_rgb_color, 0.0) + transition = kwargs.get(ATTR_TRANSITION, 0.5) + + await self._device.set_rgb_dim_level_with_time( + channelIndex=self.channel, + rgb=simple_rgb_color, + dimLevel=0.0, + onTime=0, + rampTime=transition, + ) def _convert_color(color) -> RGBColorState: diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index a55d9ea9151..632a6aac449 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -79,10 +79,19 @@ async def test_hmip_notification_light(hass, default_mock_hap): # Send all color via service call. await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id}, blocking=True + "light", + "turn_on", + {"entity_id": entity_id, "brightness_pct": "100", "transition": 100}, + blocking=True, ) - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.RED, 1.0) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level_with_time" + assert hmip_device.mock_calls[-1][2] == { + "channelIndex": 2, + "rgb": "RED", + "dimLevel": 1.0, + "onTime": 0, + "rampTime": 100.0, + } color_list = { RGBColorState.WHITE: [0.0, 0.0], @@ -101,17 +110,17 @@ async def test_hmip_notification_light(hass, default_mock_hap): {"entity_id": entity_id, "hs_color": hs_color}, blocking=True, ) - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - assert hmip_device.mock_calls[-1][1] == (2, color, 0.0392156862745098) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level_with_time" + assert hmip_device.mock_calls[-1][2] == { + "channelIndex": 2, + "dimLevel": 0.0392156862745098, + "onTime": 0, + "rampTime": 0.5, + "rgb": color, + } assert len(hmip_device.mock_calls) == service_call_counter + 8 - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - assert hmip_device.mock_calls[-1][1] == ( - 2, - RGBColorState.PURPLE, - 0.0392156862745098, - ) await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, 2) await async_manipulate_test_data( hass, hmip_device, "simpleRGBColorState", RGBColorState.PURPLE, 2 @@ -122,11 +131,17 @@ async def test_hmip_notification_light(hass, default_mock_hap): assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 await hass.services.async_call( - "light", "turn_off", {"entity_id": entity_id}, blocking=True + "light", "turn_off", {"entity_id": entity_id, "transition": 100}, blocking=True ) assert len(hmip_device.mock_calls) == service_call_counter + 11 - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.PURPLE, 0.0) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level_with_time" + assert hmip_device.mock_calls[-1][2] == { + "channelIndex": 2, + "dimLevel": 0.0, + "onTime": 0, + "rampTime": 100, + "rgb": "PURPLE", + } await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF From 99c0559a0c07e1c8a7b1418247d5f888f6b04d3d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 4 Nov 2019 13:10:42 +0100 Subject: [PATCH 161/306] Speech to Text component (#28434) * Initial commit for STT * Fix code review --- CODEOWNERS | 1 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/stt.py | 60 ++++++ homeassistant/components/demo/tts.py | 2 +- homeassistant/components/stt/__init__.py | 217 +++++++++++++++++++++ homeassistant/components/stt/const.py | 48 +++++ homeassistant/components/stt/manifest.json | 8 + homeassistant/components/stt/services.yaml | 1 + tests/components/demo/test_stt.py | 69 +++++++ tests/components/stt/__init__.py | 1 + tests/components/stt/test_init.py | 29 +++ 11 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/demo/stt.py create mode 100644 homeassistant/components/stt/__init__.py create mode 100644 homeassistant/components/stt/const.py create mode 100644 homeassistant/components/stt/manifest.json create mode 100644 homeassistant/components/stt/services.yaml create mode 100644 tests/components/demo/test_stt.py create mode 100644 tests/components/stt/__init__.py create mode 100644 tests/components/stt/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index aed575b5271..77a2ee8355b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -280,6 +280,7 @@ homeassistant/components/sql/* @dgomes homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stream/* @hunterjm +homeassistant/components/stt/* @pvizeli homeassistant/components/suez_water/* @ooii homeassistant/components/sun/* @Swamp-Ig homeassistant/components/supla/* @mwegrzynek diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index d93d217caa7..93a34794366 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -25,6 +25,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ "media_player", "notify", "sensor", + "stt", "switch", "tts", "mailbox", diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py new file mode 100644 index 00000000000..90a4cc03296 --- /dev/null +++ b/homeassistant/components/demo/stt.py @@ -0,0 +1,60 @@ +"""Support for the demo for speech to text service.""" +from typing import List + +from aiohttp import StreamReader + +from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult +from homeassistant.components.stt.const import ( + AudioBitrates, + AudioFormats, + AudioSamplerates, + AudioCodecs, + SpeechResultState, +) + +SUPPORT_LANGUAGES = ["en", "de"] + + +async def async_get_engine(hass, config): + """Set up Demo speech component.""" + return DemoProvider() + + +class DemoProvider(Provider): + """Demo speech API provider.""" + + @property + def supported_languages(self) -> List[str]: + """Return a list of supported languages.""" + return SUPPORT_LANGUAGES + + @property + def supported_formats(self) -> List[AudioFormats]: + """Return a list of supported formats.""" + return [AudioFormats.WAV] + + @property + def supported_codecs(self) -> List[AudioCodecs]: + """Return a list of supported codecs.""" + return [AudioCodecs.PCM] + + @property + def supported_bitrates(self) -> List[AudioBitrates]: + """Return a list of supported bitrates.""" + return [AudioBitrates.BITRATE_16] + + @property + def supported_samplerates(self) -> List[AudioSamplerates]: + """Return a list of supported samplerates.""" + return [AudioSamplerates.SAMPLERATE_16000, AudioSamplerates.SAMPLERATE_44100] + + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: StreamReader + ) -> SpeechResult: + """Process an audio stream to STT service.""" + + # Read available data + async for _ in stream.iter_chunked(4096): + pass + + return SpeechResult("Turn the Kitchen Lights on", SpeechResultState.SUCCESS) diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index ae083e50454..441b0cc0b3c 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -1,4 +1,4 @@ -"""Support for the demo speech service.""" +"""Support for the demo for text to speech service.""" import os import voluptuous as vol diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py new file mode 100644 index 00000000000..ea5119b5e24 --- /dev/null +++ b/homeassistant/components/stt/__init__.py @@ -0,0 +1,217 @@ +"""Provide functionality to STT.""" +from abc import ABC, abstractmethod +import asyncio +import logging +from typing import Dict, List, Optional + +from aiohttp import StreamReader, web +from aiohttp.hdrs import istr +from aiohttp.web_exceptions import ( + HTTPNotFound, + HTTPUnsupportedMediaType, + HTTPBadRequest, +) +import attr + +from homeassistant.components.http import HomeAssistantView +from homeassistant.core import callback +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform + +from .const import ( + DOMAIN, + AudioBitrates, + AudioCodecs, + AudioFormats, + AudioSamplerates, + SpeechResultState, +) + +# mypy: allow-untyped-defs, no-check-untyped-defs + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config): + """Set up STT.""" + providers = {} + + async def async_setup_platform(p_type, p_config, disc_info=None): + """Set up a TTS platform.""" + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) + if platform is None: + return + + try: + provider = await platform.async_get_engine(hass, p_config) + if provider is None: + _LOGGER.error("Error setting up platform %s", p_type) + return + + provider.name = p_type + provider.hass = hass + + providers[provider.name] = provider + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error setting up platform: %s", p_type) + return + + setup_tasks = [ + async_setup_platform(p_type, p_config) + for p_type, p_config in config_per_platform(config, DOMAIN) + ] + + if setup_tasks: + await asyncio.wait(setup_tasks) + + hass.http.register_view(SpeechToTextView(providers)) + return True + + +@attr.s +class SpeechMetadata: + """Metadata of audio stream.""" + + language: str = attr.ib() + format: AudioFormats = attr.ib() + codec: AudioCodecs = attr.ib() + bitrate: AudioBitrates = attr.ib(converter=int) + samplerate: AudioSamplerates = attr.ib(converter=int) + + +@attr.s +class SpeechResult: + """Result of audio Speech.""" + + text: str = attr.ib() + result: SpeechResultState = attr.ib() + + +class Provider(ABC): + """Represent a single STT provider.""" + + hass: Optional[HomeAssistantType] = None + name: Optional[str] = None + + @property + @abstractmethod + def supported_languages(self) -> List[str]: + """Return a list of supported languages.""" + + @property + @abstractmethod + def supported_formats(self) -> List[AudioFormats]: + """Return a list of supported formats.""" + + @property + @abstractmethod + def supported_codecs(self) -> List[AudioCodecs]: + """Return a list of supported codecs.""" + + @property + @abstractmethod + def supported_bitrates(self) -> List[AudioBitrates]: + """Return a list of supported bitrates.""" + + @property + @abstractmethod + def supported_samplerates(self) -> List[AudioSamplerates]: + """Return a list of supported samplerates.""" + + @abstractmethod + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: StreamReader + ) -> SpeechResult: + """Process an audio stream to STT service. + + Only streaming of content are allow! + """ + + @callback + def check_metadata(self, metadata: SpeechMetadata) -> bool: + """Check if given metadata supported by this provider.""" + if ( + metadata.language not in self.supported_languages + or metadata.format not in self.supported_formats + or metadata.codec not in self.supported_codecs + or metadata.bitrate not in self.supported_bitrates + or metadata.samplerate not in self.supported_samplerates + ): + return False + return True + + +class SpeechToTextView(HomeAssistantView): + """STT view to generate a text from audio stream.""" + + requires_auth = True + url = "/api/stt/{provider}" + name = "api:stt:provider" + + def __init__(self, providers: Dict[str, Provider]) -> None: + """Initialize a tts view.""" + self.providers = providers + + @staticmethod + def _metadata_from_header(request: web.Request) -> Optional[SpeechMetadata]: + """Extract metadata from header. + + X-Speech-Content: format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de_de + """ + try: + data = request.headers[istr("X-Speech-Content")].split(";") + except KeyError: + _LOGGER.warning("Missing X-Speech-Content") + return None + + # Convert Header data + args = dict() + for value in data: + value = value.strip() + args[value.partition("=")[0]] = value.partition("=")[2] + + try: + return SpeechMetadata(**args) + except TypeError as err: + _LOGGER.warning("Wrong format of X-Speech-Content: %s", err) + return None + + async def post(self, request: web.Request, provider: str) -> web.Response: + """Convert Speech (audio) to text.""" + if provider not in self.providers: + raise HTTPNotFound() + stt_provider: Provider = self.providers[provider] + + # Get metadata + metadata = self._metadata_from_header(request) + if not metadata: + raise HTTPBadRequest() + + # Check format + if not stt_provider.check_metadata(metadata): + raise HTTPUnsupportedMediaType() + + # Process audio stream + result = await stt_provider.async_process_audio_stream( + metadata, request.content + ) + + # Return result + return self.json(attr.asdict(result)) + + async def get(self, request: web.Request, provider: str) -> web.Response: + """Return provider specific audio information.""" + if provider not in self.providers: + raise HTTPNotFound() + stt_provider: Provider = self.providers[provider] + + return self.json( + { + "languages": stt_provider.supported_languages, + "formats": stt_provider.supported_formats, + "codecs": stt_provider.supported_codecs, + "samplerates": stt_provider.supported_samplerates, + "bitrates": stt_provider.supported_bitrates, + } + ) diff --git a/homeassistant/components/stt/const.py b/homeassistant/components/stt/const.py new file mode 100644 index 00000000000..dfdd91d4f96 --- /dev/null +++ b/homeassistant/components/stt/const.py @@ -0,0 +1,48 @@ +"""STT constante.""" +from enum import Enum + +DOMAIN = "stt" + + +class AudioCodecs(str, Enum): + """Supported Audio codecs.""" + + PCM = "pcm" + OPUS = "opus" + + +class AudioFormats(str, Enum): + """Supported Audio formats.""" + + WAV = "wav" + OGG = "ogg" + + +class AudioBitrates(int, Enum): + """Supported Audio bitrates.""" + + BITRATE_8 = 8 + BITRATE_16 = 16 + BITRATE_24 = 24 + BITRATE_32 = 32 + + +class AudioSamplerates(int, Enum): + """Supported Audio samplerates.""" + + SAMPLERATE_8000 = 8000 + SAMPLERATE_11000 = 11000 + SAMPLERATE_16000 = 16000 + SAMPLERATE_18900 = 18900 + SAMPLERATE_22000 = 22000 + SAMPLERATE_32000 = 32000 + SAMPLERATE_37800 = 37800 + SAMPLERATE_44100 = 44100 + SAMPLERATE_48000 = 48000 + + +class SpeechResultState(str, Enum): + """Result state of speech.""" + + SUCCESS = "success" + ERROR = "error" diff --git a/homeassistant/components/stt/manifest.json b/homeassistant/components/stt/manifest.json new file mode 100644 index 00000000000..03ea914dec1 --- /dev/null +++ b/homeassistant/components/stt/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "stt", + "name": "Stt", + "documentation": "https://www.home-assistant.io/integrations/stt", + "requirements": [], + "dependencies": ["http"], + "codeowners": ["@pvizeli"] +} diff --git a/homeassistant/components/stt/services.yaml b/homeassistant/components/stt/services.yaml new file mode 100644 index 00000000000..7172061a30a --- /dev/null +++ b/homeassistant/components/stt/services.yaml @@ -0,0 +1 @@ +# Describes the format for available STT services diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py new file mode 100644 index 00000000000..39fed7658ee --- /dev/null +++ b/tests/components/demo/test_stt.py @@ -0,0 +1,69 @@ +"""The tests for the demo stt component.""" +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components import stt + + +@pytest.fixture(autouse=True) +def setup_comp(hass): + """Set up demo component.""" + hass.loop.run_until_complete( + async_setup_component(hass, stt.DOMAIN, {"stt": {"platform": "demo"}}) + ) + + +async def test_demo_settings(hass_client): + """Test retrieve settings from demo provider.""" + client = await hass_client() + + response = await client.get("/api/stt/demo") + response_data = await response.json() + + assert response.status == 200 + assert response_data == { + "languages": ["en", "de"], + "bitrates": [16], + "samplerates": [16000, 44100], + "formats": ["wav"], + "codecs": ["pcm"], + } + + +async def test_demo_speech_no_metadata(hass_client): + """Test retrieve settings from demo provider.""" + client = await hass_client() + + response = await client.post("/api/stt/demo", data=b"Test") + assert response.status == 400 + + +async def test_demo_speech_wrong_metadata(hass_client): + """Test retrieve settings from demo provider.""" + client = await hass_client() + + response = await client.post( + "/api/stt/demo", + headers={ + "X-Speech-Content": "format=wav; codec=pcm; samplerate=8000; bitrate=16; language=de" + }, + data=b"Test", + ) + assert response.status == 415 + + +async def test_demo_speech(hass_client): + """Test retrieve settings from demo provider.""" + client = await hass_client() + + response = await client.post( + "/api/stt/demo", + headers={ + "X-Speech-Content": "format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de" + }, + data=b"Test", + ) + response_data = await response.json() + + assert response.status == 200 + assert response_data == {"text": "Turn the Kitchen Lights on", "result": "success"} diff --git a/tests/components/stt/__init__.py b/tests/components/stt/__init__.py new file mode 100644 index 00000000000..b55931a99cf --- /dev/null +++ b/tests/components/stt/__init__.py @@ -0,0 +1 @@ +"""Speech to text tests.""" diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py new file mode 100644 index 00000000000..5627d7d3e53 --- /dev/null +++ b/tests/components/stt/test_init.py @@ -0,0 +1,29 @@ +"""Test STT component setup.""" + +from homeassistant.setup import async_setup_component +from homeassistant.components import stt + + +async def test_setup_comp(hass): + """Set up demo component.""" + assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) + + +async def test_demo_settings_not_exists(hass, hass_client): + """Test retrieve settings from demo provider.""" + assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) + client = await hass_client() + + response = await client.get("/api/stt/beer") + + assert response.status == 404 + + +async def test_demo_speech_not_exists(hass, hass_client): + """Test retrieve settings from demo provider.""" + assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) + client = await hass_client() + + response = await client.post("/api/stt/beer", data=b"test") + + assert response.status == 404 From 5b85456759142c5942c5cd95770acf96be0fcd52 Mon Sep 17 00:00:00 2001 From: Jess Date: Mon, 4 Nov 2019 13:32:33 +0000 Subject: [PATCH 162/306] Add switches (on/off zones) to geniushub (#28182) * New switch platform for geniushub * Update to new geniushub-client with support for on/off zones --- .../components/geniushub/__init__.py | 14 ++++- homeassistant/components/geniushub/climate.py | 4 +- .../components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/switch.py | 55 +++++++++++++++++++ .../components/geniushub/water_heater.py | 4 +- requirements_all.txt | 2 +- 6 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/geniushub/switch.py diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index b34c46a9f26..977656149c5 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -93,7 +93,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) - for platform in ["climate", "water_heater", "sensor", "binary_sensor"]: + for platform in ["climate", "water_heater", "sensor", "binary_sensor", "switch"]: hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) return True @@ -215,8 +215,6 @@ class GeniusZone(GeniusEntity): self._zone = zone self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" - self._max_temp = self._min_temp = self._supported_features = None - @property def name(self) -> str: """Return the name of the climate device.""" @@ -228,6 +226,16 @@ class GeniusZone(GeniusEntity): status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} return {"status": status} + +class GeniusHeatingZone(GeniusZone): + """Base for Genius Heating Zones.""" + + def __init__(self, broker, zone) -> None: + """Initialize the Zone.""" + super().__init__(broker, zone) + + self._max_temp = self._min_temp = self._supported_features = None + @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 9a19edd9f8b..2221b8706c8 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -15,7 +15,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusZone +from . import DOMAIN, GeniusHeatingZone # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes HA_HVAC_TO_GH = {HVAC_MODE_OFF: "off", HVAC_MODE_HEAT: "timer"} @@ -45,7 +45,7 @@ async def async_setup_platform( ) -class GeniusClimateZone(GeniusZone, ClimateDevice): +class GeniusClimateZone(GeniusHeatingZone, ClimateDevice): """Representation of a Genius Hub climate device.""" def __init__(self, broker, zone) -> None: diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index f9e8e6eb4f0..6aa0d792c77 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": [ - "geniushub-client==0.6.28" + "geniushub-client==0.6.30" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py new file mode 100644 index 00000000000..79d14417dd4 --- /dev/null +++ b/homeassistant/components/geniushub/switch.py @@ -0,0 +1,55 @@ +"""Support for Genius Hub switch/outlet devices.""" +from homeassistant.components.switch import SwitchDevice, DEVICE_CLASS_OUTLET +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from . import DOMAIN, GeniusZone + +ATTR_DURATION = "duration" + +GH_ON_OFF_ZONE = "on / off" + + +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: + """Set up the Genius Hub switch entities.""" + if discovery_info is None: + return + + broker = hass.data[DOMAIN]["broker"] + + async_add_entities( + [ + GeniusSwitch(broker, z) + for z in broker.client.zone_objs + if z.data["type"] == GH_ON_OFF_ZONE + ] + ) + + +class GeniusSwitch(GeniusZone, SwitchDevice): + """Representation of a Genius Hub switch.""" + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_OUTLET + + @property + def is_on(self) -> bool: + """Return the current state of the on/off zone. + + The zone is considered 'on' if & only if it is override/on (e.g. timer/on is 'off'). + """ + return self._zone.data["mode"] == "override" and self._zone.data["setpoint"] + + async def async_turn_off(self, **kwargs) -> None: + """Send the zone to Timer mode. + + The zone is deemed 'off' in this mode, although the plugs may actually be on. + """ + await self._zone.set_mode("timer") + + async def async_turn_on(self, **kwargs) -> None: + """Set the zone to override/on ({'setpoint': true}) for x seconds.""" + await self._zone.set_override(1, kwargs.get(ATTR_DURATION, 3600)) diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 4141e9f8c04..e7e3278eaf6 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -9,7 +9,7 @@ from homeassistant.components.water_heater import ( from homeassistant.const import STATE_OFF from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusZone +from . import DOMAIN, GeniusHeatingZone STATE_AUTO = "auto" STATE_MANUAL = "manual" @@ -49,7 +49,7 @@ async def async_setup_platform( ) -class GeniusWaterHeater(GeniusZone, WaterHeaterDevice): +class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterDevice): """Representation of a Genius Hub water_heater device.""" def __init__(self, broker, zone) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index b831ec7af89..64cc58eeb23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.28 +geniushub-client==0.6.30 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From f3ea44cd92cdc912cfaace92e07cd820fba8c38e Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 4 Nov 2019 10:17:32 -0500 Subject: [PATCH 163/306] Cleanup Device Registry on Z-Wave Node Removal (#28240) * Remove device from device registry on node removal * Make async_get_registry from entity registry more concise * Lower log level to debug --- homeassistant/components/zwave/__init__.py | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 841b283a98d..97a904c5d99 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -13,7 +13,12 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_platform import EntityPlatform -from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.entity_registry import ( + async_get_registry as async_get_entity_registry, +) +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, @@ -376,7 +381,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DATA_DEVICES] = {} hass.data[DATA_ENTITY_VALUES] = [] - registry = await async_get_registry(hass) + registry = await async_get_entity_registry(hass) wsapi.async_load_websocket_api(hass) @@ -479,13 +484,14 @@ async def async_setup_entry(hass, config_entry): def node_removed(node): node_id = node.node_id node_key = f"node-{node_id}" - _LOGGER.info("Node Removed: %s", hass.data[DATA_DEVICES][node_key]) for key in list(hass.data[DATA_DEVICES]): + if key is None: + continue if not key.startswith(f"{node_id}-"): continue entity = hass.data[DATA_DEVICES][key] - _LOGGER.info( + _LOGGER.debug( "Removing Entity - value: %s - entity_id: %s", key, entity.entity_id ) hass.add_job(entity.node_removed()) @@ -495,6 +501,16 @@ async def async_setup_entry(hass, config_entry): hass.add_job(entity.node_removed()) del hass.data[DATA_DEVICES][node_key] + hass.add_job(_remove_device(node)) + + async def _remove_device(node): + dev_reg = await async_get_device_registry(hass) + identifier, name = node_device_id_and_name(node) + device = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + if device is not None: + _LOGGER.debug("Removing Device - %s - %s", device.id, name) + dev_reg.async_remove_device(device.id) + def network_ready(): """Handle the query of all awake nodes.""" _LOGGER.info( @@ -1208,7 +1224,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self._name = _value_name(self.values.primary) if update_ids: # Update entity ID. - ent_reg = await async_get_registry(self.hass) + ent_reg = await async_get_entity_registry(self.hass) new_entity_id = ent_reg.async_generate_entity_id( self.platform.domain, self._name, From 6a7b5657acc7a201fa4817c9e49f015a327e2e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Nov 2019 19:56:49 +0200 Subject: [PATCH 164/306] Support Huawei LTE SSDP discovery (#28214) * Support Huawei LTE SSDP discovery * Avoid KeyError on simultaneous user initiated flow Co-Authored-By: Paulus Schoutsen * Format code * Add already configured check * Initialize context in test flows * Move deviceType match to manifest * Update generated.ssdp * Add SSDP config flow test case * Remove stale debug print from tests --- .../huawei_lte/.translations/en.json | 4 +- .../components/huawei_lte/config_flow.py | 50 +++++++++++-- .../components/huawei_lte/manifest.json | 6 ++ .../components/huawei_lte/strings.json | 4 +- homeassistant/generated/ssdp.py | 6 ++ .../components/huawei_lte/test_config_flow.py | 73 ++++++++++++++----- 6 files changed, 116 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 8681e3355a4..0952b05a5cf 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "This device is already configured" + "already_configured": "This device has already been configured", + "already_in_progress": "This device is already being configured", + "not_huawei_lte": "Not a Huawei LTE device" }, "error": { "connection_failed": "Connection failed", diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 992dc33a697..1bc3753bdd7 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -19,6 +19,7 @@ from url_normalize import url_normalize import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_HOST, ATTR_NAME, ATTR_PRESENTATIONURL from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME from homeassistant.core import callback from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME @@ -52,7 +53,14 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ( ( vol.Required( - CONF_URL, default=user_input.get(CONF_URL, "") + CONF_URL, + default=user_input.get( + CONF_URL, + # https://github.com/PyCQA/pylint/issues/3167 + self.context.get( # pylint: disable=no-member + CONF_URL, "" + ), + ), ), str, ), @@ -78,6 +86,14 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle import initiated config flow.""" return await self.async_step_user(user_input) + def _already_configured(self, user_input): + """See if we already have a router matching user input configured.""" + existing_urls = { + url_normalize(entry.data[CONF_URL], default_scheme="http") + for entry in self._async_current_entries() + } + return user_input[CONF_URL] in existing_urls + async def async_step_user(self, user_input=None): """Handle user initiated config flow.""" if user_input is None: @@ -95,12 +111,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input=user_input, errors=errors ) - # See if we already have a router configured with this URL - existing_urls = { # existing entries - url_normalize(entry.data[CONF_URL], default_scheme="http") - for entry in self._async_current_entries() - } - if user_input[CONF_URL] in existing_urls: + if self._already_configured(user_input): return self.async_abort(reason="already_configured") conn = None @@ -194,6 +205,31 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=user_input) + async def async_step_ssdp(self, discovery_info): + """Handle SSDP initiated config flow.""" + # Attempt to distinguish from other non-LTE Huawei router devices, at least + # some ones we are interested in have "Mobile Wi-Fi" friendlyName. + if "mobile" not in discovery_info.get(ATTR_NAME, "").lower(): + return self.async_abort(reason="not_huawei_lte") + + # https://github.com/PyCQA/pylint/issues/3167 + url = self.context[CONF_URL] = url_normalize( # pylint: disable=no-member + discovery_info.get( + ATTR_PRESENTATIONURL, f"http://{discovery_info[ATTR_HOST]}/" + ) + ) + + if any( + url == flow["context"].get(CONF_URL) for flow in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") + + user_input = {CONF_URL: url} + if self._already_configured(user_input): + return self.async_abort(reason="already_configured") + + return await self._async_show_user_form(user_input) + class OptionsFlowHandler(config_entries.OptionsFlow): """Huawei LTE options flow.""" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index b3c4442caa9..4ea54188688 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -9,6 +9,12 @@ "stringcase==1.2.0", "url-normalize==1.4.1" ], + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Huawei" + } + ], "dependencies": [], "codeowners": [ "@scop" diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 2e76cf1b343..17684253671 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "This device is already configured" + "already_configured": "This device has already been configured", + "already_in_progress": "This device is already being configured", + "not_huawei_lte": "Not a Huawei LTE device" }, "error": { "connection_failed": "Connection failed", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 472ad6683ed..adf3a345bbe 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -16,6 +16,12 @@ SSDP = { "st": "urn:schemas-denon-com:device:ACT-Denon:1" } ], + "huawei_lte": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Huawei" + } + ], "hue": [ { "manufacturer": "Royal Philips Electronics" diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index aafa6abd57f..a9f5034fcfe 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -10,6 +10,21 @@ from homeassistant import data_entry_flow from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_URL from homeassistant.components.huawei_lte.const import DOMAIN from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler +from homeassistant.components.ssdp import ( + ATTR_HOST, + ATTR_MANUFACTURER, + ATTR_MANUFACTURERURL, + ATTR_MODEL_NAME, + ATTR_MODEL_NUMBER, + ATTR_NAME, + ATTR_PORT, + ATTR_PRESENTATIONURL, + ATTR_SERIAL, + ATTR_ST, + ATTR_UDN, + ATTR_UPNP_DEVICE_TYPE, +) + from tests.common import MockConfigEntry @@ -20,21 +35,26 @@ FIXTURE_USER_INPUT = { } -async def test_show_set_form(hass): - """Test that the setup form is served.""" +@pytest.fixture +def flow(hass): + """Get flow to test.""" flow = ConfigFlowHandler() flow.hass = hass + flow.context = {} + return flow + + +async def test_show_set_form(flow): + """Test that the setup form is served.""" result = await flow.async_step_user(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" -async def test_urlize_plain_host(hass, requests_mock): +async def test_urlize_plain_host(flow, requests_mock): """Test that plain host or IP gets converted to a URL.""" requests_mock.request(ANY, ANY, exc=ConnectionError()) - flow = ConfigFlowHandler() - flow.hass = hass host = "192.168.100.1" user_input = {**FIXTURE_USER_INPUT, CONF_URL: host} result = await flow.async_step_user(user_input=user_input) @@ -44,14 +64,12 @@ async def test_urlize_plain_host(hass, requests_mock): assert user_input[CONF_URL] == f"http://{host}/" -async def test_already_configured(hass): +async def test_already_configured(flow): """Test we reject already configured devices.""" MockConfigEntry( domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Already configured" - ).add_to_hass(hass) + ).add_to_hass(flow.hass) - flow = ConfigFlowHandler() - flow.hass = hass # Tweak URL a bit to check that doesn't fail duplicate detection result = await flow.async_step_user( user_input={ @@ -64,12 +82,10 @@ async def test_already_configured(hass): assert result["reason"] == "already_configured" -async def test_connection_error(hass, requests_mock): +async def test_connection_error(flow, requests_mock): """Test we show user form on connection error.""" requests_mock.request(ANY, ANY, exc=ConnectionError()) - flow = ConfigFlowHandler() - flow.hass = hass result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -107,15 +123,13 @@ def login_requests_mock(requests_mock): (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}), ), ) -async def test_login_error(hass, login_requests_mock, code, errors): +async def test_login_error(flow, login_requests_mock, code, errors): """Test we show user form with appropriate error on response failure.""" login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", text=f"{code}", ) - flow = ConfigFlowHandler() - flow.hass = hass result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -123,18 +137,41 @@ async def test_login_error(hass, login_requests_mock, code, errors): assert result["errors"] == errors -async def test_success(hass, login_requests_mock): +async def test_success(flow, login_requests_mock): """Test successful flow provides entry creation data.""" login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", text=f"OK", ) - flow = ConfigFlowHandler() - flow.hass = hass result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + + +async def test_ssdp(flow): + """Test SSDP discovery initiates config properly.""" + url = "http://192.168.100.1/" + result = await flow.async_step_ssdp( + discovery_info={ + ATTR_ST: "upnp:rootdevice", + ATTR_PORT: 60957, + ATTR_HOST: "192.168.100.1", + ATTR_MANUFACTURER: "Huawei", + ATTR_MANUFACTURERURL: "http://www.huawei.com/", + ATTR_MODEL_NAME: "Huawei router", + ATTR_MODEL_NUMBER: "12345678", + ATTR_NAME: "Mobile Wi-Fi", + ATTR_PRESENTATIONURL: url, + ATTR_SERIAL: "00000000", + ATTR_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + } + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert flow.context[CONF_URL] == url From aaad8eac0a49ef033cc4a4aa8ce3c039e47e0cf3 Mon Sep 17 00:00:00 2001 From: chriscla Date: Mon, 4 Nov 2019 10:39:03 -0800 Subject: [PATCH 165/306] Fire an event when nzbget download completes (#27763) * Fire an event when download completes * Rename event and use a set * Use a set comprehension * Renaming method --- homeassistant/components/nzbget/__init__.py | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index ae3ce7c5944..40a30d31743 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -81,6 +81,7 @@ def setup(hass, config): _LOGGER.debug("Successfully validated NZBGet API connection") nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api) + nzbget_data.init_download_list() nzbget_data.update() def service_handler(service): @@ -127,18 +128,50 @@ class NZBGetData: self.status = None self.available = True self._api = api + self.downloads = None + self.completed_downloads = set() def update(self): """Get the latest data from NZBGet instance.""" try: self.status = self._api.status() + self.downloads = self._api.history() + + self.check_completed_downloads() + self.available = True dispatcher_send(self.hass, DATA_UPDATED) except pynzbgetapi.NZBGetAPIException as err: self.available = False _LOGGER.error("Unable to refresh NZBGet data: %s", err) + def init_download_list(self): + """Initialize download list.""" + self.downloads = self._api.history() + self.completed_downloads = { + (x["Name"], x["Category"], x["Status"]) for x in self.downloads + } + + def check_completed_downloads(self): + """Check history for newly completed downloads.""" + + actual_completed_downloads = { + (x["Name"], x["Category"], x["Status"]) for x in self.downloads + } + + tmp_completed_downloads = list( + actual_completed_downloads.difference(self.completed_downloads) + ) + + for download in tmp_completed_downloads: + self.hass.bus.fire( + "nzbget_download_complete", + {"name": download[0], "category": download[1], "status": download[2]}, + ) + + self.completed_downloads = actual_completed_downloads + def pause_download(self): """Pause download queue.""" From 06c26f3ffc75d0de1f746caf25971efa7b80730e Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 4 Nov 2019 20:12:43 +0100 Subject: [PATCH 166/306] Add heating_type for ViCare integration (#27296) * Add heating_type for ViCare * Add additional gas heating properties * Update requirements_all * Add hvac action * Remove measurements * Remove unused property --- homeassistant/components/vicare/__init__.py | 29 +++++++++++++- homeassistant/components/vicare/climate.py | 38 +++++++++++++++++-- homeassistant/components/vicare/manifest.json | 2 +- .../components/vicare/water_heater.py | 14 ++++++- requirements_all.txt | 2 +- 5 files changed, 77 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 9fec04f2328..e091ff99970 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -1,8 +1,12 @@ """The ViCare integration.""" +import enum import logging import voluptuous as vol + from PyViCare.PyViCareDevice import Device +from PyViCare.PyViCareGazBoiler import GazBoiler +from PyViCare.PyViCareHeatPump import HeatPump import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME @@ -15,8 +19,20 @@ VICARE_PLATFORMS = ["climate", "water_heater"] DOMAIN = "vicare" VICARE_API = "api" VICARE_NAME = "name" +VICARE_HEATING_TYPE = "heating_type" CONF_CIRCUIT = "circuit" +CONF_HEATING_TYPE = "heating_type" +DEFAULT_HEATING_TYPE = "generic" + + +class HeatingType(enum.Enum): + """Possible options for heating type.""" + + generic = "generic" + gas = "gas" + heatpump = "heatpump" + CONFIG_SCHEMA = vol.Schema( { @@ -26,6 +42,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_CIRCUIT): int, vol.Optional(CONF_NAME, default="ViCare"): cv.string, + vol.Optional(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE): cv.enum( + HeatingType + ), } ) }, @@ -40,8 +59,15 @@ def setup(hass, config): if conf.get(CONF_CIRCUIT) is not None: params["circuit"] = conf[CONF_CIRCUIT] + heating_type = conf[CONF_HEATING_TYPE] + try: - vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + if heating_type == HeatingType.gas: + vicare_api = GazBoiler(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + elif heating_type == HeatingType.heatpump: + vicare_api = HeatPump(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + else: + vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) except AttributeError: _LOGGER.error( "Failed to create PyViCare API client. Please check your credentials." @@ -51,6 +77,7 @@ def setup(hass, config): hass.data[DOMAIN] = {} hass.data[DOMAIN][VICARE_API] = vicare_api hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] + hass.data[DOMAIN][VICARE_HEATING_TYPE] = heating_type for platform in VICARE_PLATFORMS: discovery.load_platform(hass, platform, DOMAIN, {}, config) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 0dcb83f758a..fe162c0c837 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -10,12 +10,16 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_AUTO, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, ) from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE from . import DOMAIN as VICARE_DOMAIN from . import VICARE_API from . import VICARE_NAME +from . import VICARE_HEATING_TYPE +from . import HeatingType _LOGGER = logging.getLogger(__name__) @@ -77,15 +81,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] add_entities( - [ViCareClimate(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", vicare_api)] + [ + ViCareClimate( + f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", + vicare_api, + heating_type, + ) + ] ) class ViCareClimate(ClimateDevice): """Representation of the ViCare heating climate device.""" - def __init__(self, name, api): + def __init__(self, name, api, heating_type): """Initialize the climate device.""" self._name = name self._state = None @@ -95,6 +106,8 @@ class ViCareClimate(ClimateDevice): self._current_mode = None self._current_temperature = None self._current_program = None + self._heating_type = heating_type + self._current_action = None def update(self): """Let HA know there has been an update from the ViCare API.""" @@ -117,7 +130,7 @@ class ViCareClimate(ClimateDevice): self._current_mode = self._api.getActiveMode() - # Update the device attributes + # Update the generic device attributes self._attributes = {} self._attributes["room_temperature"] = _room_temperature self._attributes["supply_temperature"] = _supply_temperature @@ -136,6 +149,18 @@ class ViCareClimate(ClimateDevice): "circulationpump_active" ] = self._api.getCirculationPumpActive() + # Update the specific device attributes + if self._heating_type == HeatingType.gas: + self._current_action = self._api.getBurnerActive() + + self._attributes["burner_modulation"] = self._api.getBurnerModulation() + self._attributes["boiler_temperature"] = self._api.getBoilerTemperature() + + elif self._heating_type == HeatingType.heatpump: + self._current_action = self._api.getCompressorActive() + + self._attributes["return_temperature"] = self._api.getReturnTemperature() + @property def supported_features(self): """Return the list of supported features.""" @@ -183,6 +208,13 @@ class ViCareClimate(ClimateDevice): """Return the list of available hvac modes.""" return list(HA_TO_VICARE_HVAC_HEATING) + @property + def hvac_action(self): + """Return the current hvac action.""" + if self._current_action: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + @property def min_temp(self): """Return the minimum temperature.""" diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 9f7c703fe4b..03bb16fa9bb 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "dependencies": [], "codeowners": ["@oischinger"], - "requirements": ["PyViCare==0.1.1"] + "requirements": ["PyViCare==0.1.2"] } diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 71c0f6c2aef..7c4968ad0a4 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -10,6 +10,7 @@ from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE from . import DOMAIN as VICARE_DOMAIN from . import VICARE_API from . import VICARE_NAME +from . import VICARE_HEATING_TYPE _LOGGER = logging.getLogger(__name__) @@ -46,22 +47,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] add_entities( - [ViCareWater(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", vicare_api)] + [ + ViCareWater( + f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", + vicare_api, + heating_type, + ) + ] ) class ViCareWater(WaterHeaterDevice): """Representation of the ViCare domestic hot water device.""" - def __init__(self, name, api): + def __init__(self, name, api, heating_type): """Initialize the DHW water_heater device.""" self._name = name self._state = None self._api = api + self._attributes = {} self._target_temperature = None self._current_temperature = None self._current_mode = None + self._heating_type = heating_type def update(self): """Let HA know there has been an update from the ViCare API.""" diff --git a/requirements_all.txt b/requirements_all.txt index 64cc58eeb23..8635b19ae06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -78,7 +78,7 @@ PySocks==1.7.1 PyTransportNSW==0.1.1 # homeassistant.components.vicare -PyViCare==0.1.1 +PyViCare==0.1.2 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.12.4 From bbe0cf3a0c1c7b474be8f45b128a741d5d532985 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Mon, 4 Nov 2019 20:52:55 +0100 Subject: [PATCH 167/306] Bump version for asuswrt to 1.1.22 (#28322) * Bumping version * Fix requirements * Fix requirements --- homeassistant/components/asuswrt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 3fcdcf42ab1..3d8cebce096 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -3,7 +3,7 @@ "name": "Asuswrt", "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": [ - "aioasuswrt==1.1.21" + "aioasuswrt==1.1.22" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8635b19ae06..7ceb7351eb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -130,7 +130,7 @@ aio_geojson_geonetnz_quakes==0.10 aioambient==0.3.2 # homeassistant.components.asuswrt -aioasuswrt==1.1.21 +aioasuswrt==1.1.22 # homeassistant.components.automatic aioautomatic==0.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6499ac04681..9395c0cefad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,7 +62,7 @@ aio_geojson_geonetnz_quakes==0.10 aioambient==0.3.2 # homeassistant.components.asuswrt -aioasuswrt==1.1.21 +aioasuswrt==1.1.22 # homeassistant.components.automatic aioautomatic==0.6.5 From e4196892294d3cfc0c4d9dc4d22dbbb80348093d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 4 Nov 2019 21:38:18 +0100 Subject: [PATCH 168/306] Add config endpoint for scene (#28429) * Add config endpoint for scene * Add scenes to default config * Fix test and add context to websocket service call * Update scene.py * Add tests --- homeassistant/components/config/__init__.py | 1 + homeassistant/components/config/scene.py | 65 ++++++++ .../components/homeassistant/scene.py | 15 +- .../components/websocket_api/commands.py | 4 +- homeassistant/config.py | 6 + tests/components/config/test_scene.py | 144 ++++++++++++++++++ tests/test_config.py | 4 + 7 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/config/scene.py create mode 100644 tests/components/config/test_scene.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 569d1de6a02..5a66c1fc5d4 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -25,6 +25,7 @@ SECTIONS = ( "entity_registry", "group", "script", + "scene", ) ON_DEMAND = ("zwave",) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py new file mode 100644 index 00000000000..6e77dae0826 --- /dev/null +++ b/homeassistant/components/config/scene.py @@ -0,0 +1,65 @@ +"""Provide configuration end points for Scenes.""" +from collections import OrderedDict +import uuid + +from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import CONF_ID, SERVICE_RELOAD +from homeassistant.config import SCENE_CONFIG_PATH +import homeassistant.helpers.config_validation as cv + +from . import EditIdBasedConfigView + + +async def async_setup(hass): + """Set up the Scene config API.""" + + async def hook(hass): + """post_write_hook for Config View that reloads scenes.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + + hass.http.register_view( + EditSceneConfigView( + DOMAIN, + "config", + SCENE_CONFIG_PATH, + cv.string, + PLATFORM_SCHEMA, + post_write_hook=hook, + ) + ) + return True + + +class EditSceneConfigView(EditIdBasedConfigView): + """Edit scene config.""" + + def _write_value(self, hass, data, config_key, new_value): + """Set value.""" + index = None + for index, cur_value in enumerate(data): + # When people copy paste their scenes to the config file, + # they sometimes forget to add IDs. Fix it here. + if CONF_ID not in cur_value: + cur_value[CONF_ID] = uuid.uuid4().hex + + elif cur_value[CONF_ID] == config_key: + break + else: + cur_value = dict() + cur_value[CONF_ID] = config_key + index = len(data) + data.append(cur_value) + + # Iterate through some keys that we want to have ordered in the output + updated_value = OrderedDict() + for key in ("id", "name", "entities"): + if key in cur_value: + updated_value[key] = cur_value[key] + if key in new_value: + updated_value[key] = new_value[key] + + # We cover all current fields above, but just in case we start + # supporting more fields in the future. + updated_value.update(cur_value) + updated_value.update(new_value) + data[index] = updated_value diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 084c950bf17..f011dae150f 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -8,6 +8,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, CONF_ENTITIES, + CONF_ID, CONF_NAME, CONF_PLATFORM, STATE_OFF, @@ -160,7 +161,11 @@ def _process_scenes_config(hass, async_add_entities, config): return async_add_entities( - HomeAssistantScene(hass, SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES])) + HomeAssistantScene( + hass, + SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES]), + scene.get(CONF_ID), + ) for scene in scene_config ) @@ -168,8 +173,9 @@ def _process_scenes_config(hass, async_add_entities, config): class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" - def __init__(self, hass, scene_config): + def __init__(self, hass, scene_config, scene_id=None): """Initialize the scene.""" + self._id = scene_id self.hass = hass self.scene_config = scene_config @@ -181,7 +187,10 @@ class HomeAssistantScene(Scene): @property def device_state_attributes(self): """Return the scene state attributes.""" - return {ATTR_ENTITY_ID: list(self.scene_config.states)} + attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)} + if self._id is not None: + attributes[CONF_ID] = self._id + return attributes async def async_activate(self): """Activate scene. Try to get entities into requested state.""" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 9d46238b241..f30ee816914 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -132,7 +132,9 @@ async def handle_call_service(hass, connection, msg): blocking, connection.context(msg), ) - connection.send_message(messages.result_message(msg["id"])) + connection.send_message( + messages.result_message(msg["id"], {"context": connection.context(msg)}) + ) except ServiceNotFound as err: if err.domain == msg["domain"] and err.service == msg["service"]: connection.send_message( diff --git a/homeassistant/config.py b/homeassistant/config.py index 9f49889791e..864ced6a16a 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -69,6 +69,7 @@ DATA_CUSTOMIZE = "hass_customize" GROUP_CONFIG_PATH = "groups.yaml" AUTOMATION_CONFIG_PATH = "automations.yaml" SCRIPT_CONFIG_PATH = "scripts.yaml" +SCENE_CONFIG_PATH = "scenes.yaml" DEFAULT_CONFIG = f""" # Configure a default setup of Home Assistant (frontend, api, etc) @@ -85,6 +86,7 @@ tts: group: !include {GROUP_CONFIG_PATH} automation: !include {AUTOMATION_CONFIG_PATH} script: !include {SCRIPT_CONFIG_PATH} +scene: !include {SCENE_CONFIG_PATH} """ DEFAULT_SECRETS = """ # Use this file to store secrets like usernames and passwords. @@ -261,6 +263,7 @@ def _write_default_config(config_dir: str) -> Optional[str]: group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH) automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH) script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH) + scene_yaml_path = os.path.join(config_dir, SCENE_CONFIG_PATH) # Writing files with YAML does not create the most human readable results # So we're hard coding a YAML template. @@ -283,6 +286,9 @@ def _write_default_config(config_dir: str) -> Optional[str]: with open(script_yaml_path, "wt"): pass + with open(scene_yaml_path, "wt"): + pass + return config_path except OSError: diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py new file mode 100644 index 00000000000..b40c895b620 --- /dev/null +++ b/tests/components/config/test_scene.py @@ -0,0 +1,144 @@ +"""Test Automation config panel.""" +import json +from unittest.mock import patch + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components import config +from homeassistant.util.yaml import dump + + +async def test_update_scene(hass, hass_client): + """Test updating a scene.""" + with patch.object(config, "SECTIONS", ["scene"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + orig_data = [{"id": "light_on"}, {"id": "light_off"}] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + data = dump(data) + written.append(data) + + with patch("homeassistant.components.config._read", mock_read), patch( + "homeassistant.components.config._write", mock_write + ): + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + + assert resp.status == 200 + result = await resp.json() + assert result == {"result": "ok"} + + assert len(written) == 1 + written_yaml = written[0] + assert ( + written_yaml + == """- id: light_on +- id: light_off + name: Lights off + entities: + light.bedroom: + state: 'off' +""" + ) + + +async def test_bad_formatted_scene(hass, hass_client): + """Test that we handle scene without ID.""" + with patch.object(config, "SECTIONS", ["scene"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + orig_data = [ + { + # No ID + "entities": {"light.bedroom": "on"} + }, + {"id": "light_off"}, + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch("homeassistant.components.config._read", mock_read), patch( + "homeassistant.components.config._write", mock_write + ): + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + + assert resp.status == 200 + result = await resp.json() + assert result == {"result": "ok"} + + # Verify ID added to orig_data + assert "id" in orig_data[0] + + assert orig_data[1] == { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + + +async def test_delete_scene(hass, hass_client): + """Test deleting a scene.""" + with patch.object(config, "SECTIONS", ["scene"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + orig_data = [{"id": "light_on"}, {"id": "light_off"}] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch("homeassistant.components.config._read", mock_read), patch( + "homeassistant.components.config._write", mock_write + ): + resp = await client.delete("/api/config/scene/config/light_on") + + assert resp.status == 200 + result = await resp.json() + assert result == {"result": "ok"} + + assert len(written) == 1 + assert written[0][0]["id"] == "light_off" diff --git a/tests/test_config.py b/tests/test_config.py index dab51f59176..1c872369096 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -44,6 +44,7 @@ VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE) GROUP_PATH = os.path.join(CONFIG_DIR, config_util.GROUP_CONFIG_PATH) AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH) SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH) +SCENES_PATH = os.path.join(CONFIG_DIR, config_util.SCENE_CONFIG_PATH) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -75,6 +76,9 @@ def teardown(): if os.path.isfile(SCRIPTS_PATH): os.remove(SCRIPTS_PATH) + if os.path.isfile(SCENES_PATH): + os.remove(SCENES_PATH) + async def test_create_default_config(hass): """Test creation of default config.""" From fe749fc0f8eaba9ccea5b389e1b91be2d684d363 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Mon, 4 Nov 2019 12:49:11 -0800 Subject: [PATCH 169/306] Fix sensor device in the Abode component (#28516) * Fix for occupancy sensor unique_id * Add check for sensor attributes before adding entity * Fixes temperature key issue * Clean up code with better use of keys * Code clean up --- homeassistant/components/abode/sensor.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index e25921f295f..6ee0cf59cbf 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -16,9 +16,9 @@ _LOGGER = logging.getLogger(__name__) # Sensor types: Name, icon SENSOR_TYPES = { - "temp": ["Temperature", DEVICE_CLASS_TEMPERATURE], - "humidity": ["Humidity", DEVICE_CLASS_HUMIDITY], - "lux": ["Lux", DEVICE_CLASS_ILLUMINANCE], + CONST.TEMP_STATUS_KEY: ["Temperature", DEVICE_CLASS_TEMPERATURE], + CONST.HUMI_STATUS_KEY: ["Humidity", DEVICE_CLASS_HUMIDITY], + CONST.LUX_STATUS_KEY: ["Lux", DEVICE_CLASS_ILLUMINANCE], } @@ -29,15 +29,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for an Abode device.""" - data = hass.data[DOMAIN] + entities = [] - devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): for sensor_type in SENSOR_TYPES: - devices.append(AbodeSensor(data, device, sensor_type)) + if sensor_type not in device.get_value(CONST.STATUSES_KEY): + continue + entities.append(AbodeSensor(data, device, sensor_type)) - async_add_entities(devices) + async_add_entities(entities) class AbodeSensor(AbodeDevice): @@ -62,6 +63,11 @@ class AbodeSensor(AbodeDevice): """Return the device class.""" return self._device_class + @property + def unique_id(self): + """Return a unique ID to use for this device.""" + return f"{self._device.device_uuid}-{self._sensor_type}" + @property def state(self): """Return the state of the sensor.""" From 6e58a0c9964e5a1caeb56199d09ba0364532c75b Mon Sep 17 00:00:00 2001 From: Thom Troy Date: Mon, 4 Nov 2019 20:49:53 +0000 Subject: [PATCH 170/306] Update ephember library version (#28507) * update ephember library version * update requirements_all.txt for new pyephember version * update imports to top of module --- homeassistant/components/ephember/climate.py | 67 ++++++++++--------- .../components/ephember/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 0e35b8bbee7..c189b2d62b8 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -1,23 +1,35 @@ """Support for the EPH Controls Ember themostats.""" -import logging from datetime import timedelta +import logging + +from pyephember.pyephember import ( + EphEmber, + ZoneMode, + zone_current_temperature, + zone_is_active, + zone_is_boost_active, + zone_is_hot_water, + zone_mode, + zone_name, + zone_target_temperature, +) import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - SUPPORT_AUX_HEAT, - SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_OFF, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_AUX_HEAT, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, - TEMP_CELSIUS, - CONF_USERNAME, CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -43,8 +55,6 @@ HA_STATE_TO_EPH = {value: key for key, value in EPH_TO_HA_STATE.items()} def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ephember thermostat.""" - from pyephember.pyephember import EphEmber - username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -61,14 +71,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class EphEmberThermostat(ClimateDevice): - """Representation of a HeatmiserV3 thermostat.""" + """Representation of a EphEmber thermostat.""" def __init__(self, ember, zone): """Initialize the thermostat.""" self._ember = ember - self._zone_name = zone["name"] + self._zone_name = zone_name(zone) self._zone = zone - self._hot_water = zone["isHotWater"] + self._hot_water = zone_is_hot_water(zone) @property def supported_features(self): @@ -91,12 +101,12 @@ class EphEmberThermostat(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self._zone["currentTemperature"] + return zone_current_temperature(self._zone) @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._zone["targetTemperature"] + return zone_target_temperature(self._zone) @property def target_temperature_step(self): @@ -104,12 +114,12 @@ class EphEmberThermostat(ClimateDevice): if self._hot_water: return None - return 1 + return 0.5 @property def hvac_action(self): """Return current HVAC action.""" - if self._zone["isCurrentlyActive"]: + if zone_is_active(self._zone): return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE @@ -117,9 +127,7 @@ class EphEmberThermostat(ClimateDevice): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" - from pyephember.pyephember import ZoneMode - - mode = ZoneMode(self._zone["mode"]) + mode = zone_mode(self._zone) return self.map_mode_eph_hass(mode) @property @@ -138,12 +146,13 @@ class EphEmberThermostat(ClimateDevice): @property def is_aux_heat(self): """Return true if aux heater.""" - return self._zone["isBoostActive"] + + return zone_is_boost_active(self._zone) def turn_aux_heat_on(self): """Turn auxiliary heater on.""" self._ember.activate_boost_by_name( - self._zone_name, self._zone["targetTemperature"] + self._zone_name, zone_target_temperature(self._zone) ) def turn_aux_heat_off(self): @@ -165,24 +174,24 @@ class EphEmberThermostat(ClimateDevice): if temperature > self.max_temp or temperature < self.min_temp: return - self._ember.set_target_temperture_by_name(self._zone_name, int(temperature)) + self._ember.set_target_temperture_by_name(self._zone_name, temperature) @property def min_temp(self): """Return the minimum temperature.""" # Hot water temp doesn't support being changed if self._hot_water: - return self._zone["targetTemperature"] + return zone_target_temperature(self._zone) - return 5 + return 5.0 @property def max_temp(self): """Return the maximum temperature.""" if self._hot_water: - return self._zone["targetTemperature"] + return zone_target_temperature(self._zone) - return 35 + return 35.0 def update(self): """Get the latest data.""" @@ -191,8 +200,6 @@ class EphEmberThermostat(ClimateDevice): @staticmethod def map_mode_hass_eph(operation_mode): """Map from home assistant mode to eph mode.""" - from pyephember.pyephember import ZoneMode - return getattr(ZoneMode, HA_STATE_TO_EPH.get(operation_mode), None) @staticmethod diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index 7509e627621..e05d21c0c02 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -3,7 +3,7 @@ "name": "Ephember", "documentation": "https://www.home-assistant.io/integrations/ephember", "requirements": [ - "pyephember==0.2.0" + "pyephember==0.3.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7ceb7351eb7..04a858ce3b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1189,7 +1189,7 @@ pyemby==1.6 pyenvisalink==4.0 # homeassistant.components.ephember -pyephember==0.2.0 +pyephember==0.3.1 # homeassistant.components.everlights pyeverlights==0.1.0 From f5fb9fc580e6419907513edfd6b687cbfc090845 Mon Sep 17 00:00:00 2001 From: Marius Flage Date: Mon, 4 Nov 2019 21:54:36 +0100 Subject: [PATCH 171/306] Checking state before actually sending a new state change. Some projectors return ERR if you try to turn off a projector that's already off. (#28529) --- homeassistant/components/pjlink/media_player.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 6474165a6cd..ea35fe7fb75 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -158,13 +158,15 @@ class PjLinkDevice(MediaPlayerDevice): def turn_off(self): """Turn projector off.""" - with self.projector() as projector: - projector.set_power("off") + if self._pwstate == STATE_ON: + with self.projector() as projector: + projector.set_power("off") def turn_on(self): """Turn projector on.""" - with self.projector() as projector: - projector.set_power("on") + if self._pwstate == STATE_OFF: + with self.projector() as projector: + projector.set_power("on") def mute_volume(self, mute): """Mute (true) of unmute (false) media player.""" From 83a9f4ddb83ed5f428f124da1d2e1c4ebbf3793b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Nov 2019 14:01:10 -0800 Subject: [PATCH 172/306] rate is a separate word (#28535) --- homeassistant/components/demo/stt.py | 16 ++++++++-------- homeassistant/components/stt/__init__.py | 24 ++++++++++++------------ homeassistant/components/stt/const.py | 8 ++++---- tests/components/demo/test_stt.py | 8 ++++---- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index 90a4cc03296..fdd89dc4c9c 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -5,9 +5,9 @@ from aiohttp import StreamReader from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult from homeassistant.components.stt.const import ( - AudioBitrates, + AudioBitRates, AudioFormats, - AudioSamplerates, + AudioSampleRates, AudioCodecs, SpeechResultState, ) @@ -39,14 +39,14 @@ class DemoProvider(Provider): return [AudioCodecs.PCM] @property - def supported_bitrates(self) -> List[AudioBitrates]: - """Return a list of supported bitrates.""" - return [AudioBitrates.BITRATE_16] + def supported_bit_rates(self) -> List[AudioBitRates]: + """Return a list of supported bit rates.""" + return [AudioBitRates.BITRATE_16] @property - def supported_samplerates(self) -> List[AudioSamplerates]: - """Return a list of supported samplerates.""" - return [AudioSamplerates.SAMPLERATE_16000, AudioSamplerates.SAMPLERATE_44100] + def supported_sample_rates(self) -> List[AudioSampleRates]: + """Return a list of supported sample rates.""" + return [AudioSampleRates.SAMPLERATE_16000, AudioSampleRates.SAMPLERATE_44100] async def async_process_audio_stream( self, metadata: SpeechMetadata, stream: StreamReader diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index ea5119b5e24..b781c4666ae 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -21,10 +21,10 @@ from homeassistant.setup import async_prepare_setup_platform from .const import ( DOMAIN, - AudioBitrates, + AudioBitRates, AudioCodecs, AudioFormats, - AudioSamplerates, + AudioSampleRates, SpeechResultState, ) @@ -76,8 +76,8 @@ class SpeechMetadata: language: str = attr.ib() format: AudioFormats = attr.ib() codec: AudioCodecs = attr.ib() - bitrate: AudioBitrates = attr.ib(converter=int) - samplerate: AudioSamplerates = attr.ib(converter=int) + bit_rate: AudioBitRates = attr.ib(converter=int) + sample_rate: AudioSampleRates = attr.ib(converter=int) @attr.s @@ -111,13 +111,13 @@ class Provider(ABC): @property @abstractmethod - def supported_bitrates(self) -> List[AudioBitrates]: - """Return a list of supported bitrates.""" + def supported_bit_rates(self) -> List[AudioBitRates]: + """Return a list of supported bit_rates.""" @property @abstractmethod - def supported_samplerates(self) -> List[AudioSamplerates]: - """Return a list of supported samplerates.""" + def supported_sample_rates(self) -> List[AudioSampleRates]: + """Return a list of supported sample_rates.""" @abstractmethod async def async_process_audio_stream( @@ -135,8 +135,8 @@ class Provider(ABC): metadata.language not in self.supported_languages or metadata.format not in self.supported_formats or metadata.codec not in self.supported_codecs - or metadata.bitrate not in self.supported_bitrates - or metadata.samplerate not in self.supported_samplerates + or metadata.bit_rate not in self.supported_bit_rates + or metadata.sample_rate not in self.supported_sample_rates ): return False return True @@ -211,7 +211,7 @@ class SpeechToTextView(HomeAssistantView): "languages": stt_provider.supported_languages, "formats": stt_provider.supported_formats, "codecs": stt_provider.supported_codecs, - "samplerates": stt_provider.supported_samplerates, - "bitrates": stt_provider.supported_bitrates, + "sample_rates": stt_provider.supported_sample_rates, + "bit_rates": stt_provider.supported_bit_rates, } ) diff --git a/homeassistant/components/stt/const.py b/homeassistant/components/stt/const.py index dfdd91d4f96..c653bcc3bd5 100644 --- a/homeassistant/components/stt/const.py +++ b/homeassistant/components/stt/const.py @@ -18,8 +18,8 @@ class AudioFormats(str, Enum): OGG = "ogg" -class AudioBitrates(int, Enum): - """Supported Audio bitrates.""" +class AudioBitRates(int, Enum): + """Supported Audio bit_rates.""" BITRATE_8 = 8 BITRATE_16 = 16 @@ -27,8 +27,8 @@ class AudioBitrates(int, Enum): BITRATE_32 = 32 -class AudioSamplerates(int, Enum): - """Supported Audio samplerates.""" +class AudioSampleRates(int, Enum): + """Supported Audio sample_rates.""" SAMPLERATE_8000 = 8000 SAMPLERATE_11000 = 11000 diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index 39fed7658ee..782d89688c7 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -23,8 +23,8 @@ async def test_demo_settings(hass_client): assert response.status == 200 assert response_data == { "languages": ["en", "de"], - "bitrates": [16], - "samplerates": [16000, 44100], + "bit_rates": [16], + "sample_rates": [16000, 44100], "formats": ["wav"], "codecs": ["pcm"], } @@ -45,7 +45,7 @@ async def test_demo_speech_wrong_metadata(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; samplerate=8000; bitrate=16; language=de" + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; language=de" }, data=b"Test", ) @@ -59,7 +59,7 @@ async def test_demo_speech(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de" + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; language=de" }, data=b"Test", ) From 16a80beb4379b99f681f3cb019be0940efc49430 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Nov 2019 14:14:57 -0800 Subject: [PATCH 173/306] Fix scaffold --- script/scaffold/gather_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 12cb319d188..6a69040a6d7 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -120,7 +120,7 @@ def _load_existing_integration(domain) -> Info: manifest = json.loads((COMPONENT_DIR / domain / "manifest.json").read_text()) - return Info(domain=domain, name=manifest["name"]) + return Info(domain=domain, name=manifest["name"], is_new=False) def _gather_info(fields) -> dict: From ade60742d4667452a4297c8d664bf06f7fae2d9b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 5 Nov 2019 00:31:48 +0000 Subject: [PATCH 174/306] [ci skip] Translation update --- homeassistant/components/huawei_lte/.translations/en.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 0952b05a5cf..8681e3355a4 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "This device has already been configured", - "already_in_progress": "This device is already being configured", - "not_huawei_lte": "Not a Huawei LTE device" + "already_configured": "This device is already configured" }, "error": { "connection_failed": "Connection failed", From fb0e20543e6eae5f945f607017bcde41d678315c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 5 Nov 2019 02:58:17 +0100 Subject: [PATCH 175/306] Update Plugwise (#28237) * Plugwise update * Fix DEFAULT_NAME * Fix pylint errors * Remove showing of DHW-status * Remove `if not None` where possible * Forgot to remove dhw-related code * Updated w.r.t. comments from MartinHjelmare * Remove the illuminance attribute - move to sensor * Update homeassistant/components/plugwise/climate.py Co-Authored-By: Martin Hjelmare * Breaking lines * Remove thermostat_temperature * Try fix lint errors. * Remove spaces * Remove more spaces --- CODEOWNERS | 2 +- homeassistant/components/plugwise/climate.py | 155 +++++++++++++----- .../components/plugwise/manifest.json | 4 +- requirements_all.txt | 2 +- 4 files changed, 118 insertions(+), 45 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 77a2ee8355b..ce170df3602 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -229,7 +229,7 @@ homeassistant/components/pi_hole/* @fabaff @johnluetke homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren -homeassistant/components/plugwise/* @laetificat @CoMPaTech +homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index b3b4cf3c1d4..fa1ac86941b 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -9,9 +9,11 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, CURRENT_HVAC_IDLE, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO, - HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -33,6 +35,7 @@ _LOGGER = logging.getLogger(__name__) # Configuration directives CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" +CONF_LEGACY = "legacy_anna" # Default directives DEFAULT_NAME = "Plugwise Thermostat" @@ -44,7 +47,8 @@ DEFAULT_MIN_TEMP = 4 DEFAULT_MAX_TEMP = 30 # HVAC modes -ATTR_HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_OFF] +HVAC_MODES_1 = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] +HVAC_MODES_2 = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO] # Read platform configuration PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -52,6 +56,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_LEGACY, default=False): cv.boolean, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): cv.positive_int, @@ -67,6 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config[CONF_PASSWORD], config[CONF_HOST], config[CONF_PORT], + config[CONF_LEGACY], ) try: api.ping_anna_thermostat() @@ -92,15 +98,28 @@ class ThermostatDevice(ClimateDevice): self._name = name self._domain_objects = None self._outdoor_temperature = None - self._active_schema = None + self._selected_schema = None self._preset_mode = None - self._hvac_modes = ATTR_HVAC_MODES + self._presets = None + self._presets_list = None + self._heating_status = None + self._cooling_status = None + self._schema_names = None + self._schema_status = None + self._current_temperature = None + self._thermostat_temperature = None + self._boiler_temperature = None + self._water_pressure = None + self._schedule_temperature = None + self._hvac_mode = None @property def hvac_action(self): """Return the current action.""" - if self._api.get_heating_status(self._domain_objects): + if self._heating_status: return CURRENT_HVAC_HEAT + if self._cooling_status: + return CURRENT_HVAC_COOL return CURRENT_HVAC_IDLE @property @@ -122,49 +141,80 @@ class ThermostatDevice(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" attributes = {} - attributes["outdoor_temperature"] = self._outdoor_temperature - attributes["available_schemas"] = self._api.get_schema_names( - self._domain_objects - ) - attributes["active_schema"] = self._active_schema + if self._outdoor_temperature: + attributes["outdoor_temperature"] = self._outdoor_temperature + attributes["available_schemas"] = self._schema_names + attributes["selected_schema"] = self._selected_schema + if self._boiler_temperature: + attributes["boiler_temperature"] = self._boiler_temperature + if self._water_pressure: + attributes["water_pressure"] = self._water_pressure return attributes - def update(self): - """Update the data from the thermostat.""" - _LOGGER.debug("Update called") - self._domain_objects = self._api.get_domain_objects() - self._outdoor_temperature = self._api.get_outdoor_temperature( - self._domain_objects - ) - self._active_schema = self._api.get_active_schema_name(self._domain_objects) + @property + def preset_modes(self): + """Return the available preset modes list. + + And make the presets with their temperatures available. + """ + return self._presets_list + + @property + def hvac_modes(self): + """Return the available hvac modes list.""" + if self._heating_status is not None: + if self._cooling_status is not None: + return HVAC_MODES_2 + return HVAC_MODES_1 + return None @property def hvac_mode(self): """Return current active hvac state.""" - if self._api.get_schema_state(self._domain_objects): + if self._schema_status: return HVAC_MODE_AUTO - return HVAC_MODE_OFF + if self._heating_status: + if self._cooling_status: + return HVAC_MODE_HEAT_COOL + return HVAC_MODE_HEAT + return None + + @property + def target_temperature(self): + """Return the target_temperature. + + From the XML the thermostat-value is used because it updates 'immediately' + compared to the target_temperature-value. This way the information on the card + is "immediately" updated after changing the preset, temperature, etc. + """ + return self._thermostat_temperature @property def preset_mode(self): - """Return the active preset mode.""" - return self._api.get_current_preset(self._domain_objects) + """Return the active selected schedule-name. - @property - def preset_modes(self): - """Return the available preset modes list without values.""" - presets = list(self._api.get_presets(self._domain_objects)) - return presets - - @property - def hvac_modes(self): - """Return the available hvac modes list.""" - return self._hvac_modes + Or return the active preset, or return Temporary in case of a manual change + in the set-temperature with a weekschedule active, + or return Manual in case of a manual change and no weekschedule active. + """ + if self._presets: + presets = self._presets + preset_temperature = presets.get(self._preset_mode, "none") + if self.hvac_mode == HVAC_MODE_AUTO: + if self._thermostat_temperature == self._schedule_temperature: + return "{}".format(self._selected_schema) + if self._thermostat_temperature == preset_temperature: + return self._preset_mode + return "Temporary" + if self._thermostat_temperature != preset_temperature: + return "Manual" + return self._preset_mode + return None @property def current_temperature(self): - """Return the current temperature.""" - return self._api.get_temperature(self._domain_objects) + """Return the current room temperature.""" + return self._current_temperature @property def min_temp(self): @@ -176,11 +226,6 @@ class ThermostatDevice(ClimateDevice): """Return the maximum temperature possible to set.""" return self._max_temp - @property - def target_temperature(self): - """Return the target temperature.""" - return self._api.get_target_temperature(self._domain_objects) - @property def temperature_unit(self): """Return the unit of measured temperature.""" @@ -203,11 +248,39 @@ class ThermostatDevice(ClimateDevice): if hvac_mode == HVAC_MODE_AUTO: schema_mode = "true" self._api.set_schema_state( - self._domain_objects, self._active_schema, schema_mode + self._domain_objects, self._selected_schema, schema_mode ) def set_preset_mode(self, preset_mode): """Set the preset mode.""" _LOGGER.debug("Changing preset mode") - self._preset_mode = preset_mode self._api.set_preset(self._domain_objects, preset_mode) + + def update(self): + """Update the data from the thermostat.""" + _LOGGER.debug("Update called") + self._domain_objects = self._api.get_domain_objects() + self._outdoor_temperature = self._api.get_outdoor_temperature( + self._domain_objects + ) + self._selected_schema = self._api.get_active_schema_name(self._domain_objects) + self._preset_mode = self._api.get_current_preset(self._domain_objects) + self._presets = self._api.get_presets(self._domain_objects) + self._presets_list = list(self._api.get_presets(self._domain_objects)) + self._heating_status = self._api.get_heating_status(self._domain_objects) + self._cooling_status = self._api.get_cooling_status(self._domain_objects) + self._schema_names = self._api.get_schema_names(self._domain_objects) + self._schema_status = self._api.get_schema_state(self._domain_objects) + self._current_temperature = self._api.get_current_temperature( + self._domain_objects + ) + self._thermostat_temperature = self._api.get_thermostat_temperature( + self._domain_objects + ) + self._schedule_temperature = self._api.get_schedule_temperature( + self._domain_objects + ) + self._boiler_temperature = self._api.get_boiler_temperature( + self._domain_objects + ) + self._water_pressure = self._api.get_water_pressure(self._domain_objects) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 1069c0bcdf0..e786b6a7f8e 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -3,6 +3,6 @@ "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", "dependencies": [], - "codeowners": ["@laetificat","@CoMPaTech"], - "requirements": ["haanna==0.10.1"] + "codeowners": ["@laetificat","@CoMPaTech","@bouwew"], + "requirements": ["haanna==0.13.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 04a858ce3b9..282d04ac77c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -613,7 +613,7 @@ ha-ffmpeg==2.0 ha-philipsjs==0.0.8 # homeassistant.components.plugwise -haanna==0.10.1 +haanna==0.13.5 # homeassistant.components.habitica habitipy==0.2.0 From ef20f0985a318a81b13f6c90c5607eb9460a0c57 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 5 Nov 2019 06:15:58 +0100 Subject: [PATCH 176/306] Improve scene.create service (#28533) * Improve scene.create service * Typo * Improve coverage * Add from_yaml attribute * from_service instead of from_yaml --- .../components/homeassistant/scene.py | 15 ++++--- tests/components/homeassistant/test_scene.py | 39 ++++++++++++++++--- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index f011dae150f..c505d1534de 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -141,11 +141,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Create a scene.""" scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], call.data[CONF_ENTITIES]) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" - if hass.states.get(entity_id) is not None: - _LOGGER.warning("The scene %s already exists", entity_id) - return - - async_add_entities([HomeAssistantScene(hass, scene_config)]) + old = platform.entities.get(entity_id) + if old is not None: + if not old.from_service: + _LOGGER.warning("The scene %s already exists", entity_id) + return + await platform.async_remove_entity(entity_id) + async_add_entities([HomeAssistantScene(hass, scene_config, from_service=True)]) hass.services.async_register( SCENE_DOMAIN, SERVICE_CREATE, create_service, CREATE_SCENE_SCHEMA @@ -173,11 +175,12 @@ def _process_scenes_config(hass, async_add_entities, config): class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" - def __init__(self, hass, scene_config, scene_id=None): + def __init__(self, hass, scene_config, scene_id=None, from_service=False): """Initialize the scene.""" self._id = scene_id self.hass = hass self.scene_config = scene_config + self.from_service = from_service @property def name(self): diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 08e40e23d12..25ce6088a51 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -55,8 +55,13 @@ async def test_apply_service(hass): async def test_create_service(hass, caplog): """Test the create service.""" - assert await async_setup_component(hass, "scene", {}) + assert await async_setup_component( + hass, + "scene", + {"scene": {"name": "hallo_2", "entities": {"light.kitchen": "on"}}}, + ) assert hass.states.get("scene.hallo") is None + assert hass.states.get("scene.hallo_2") is not None assert await hass.services.async_call( "scene", @@ -67,8 +72,8 @@ async def test_create_service(hass, caplog): }, blocking=True, ) - await hass.async_block_till_done() + scene = hass.states.get("scene.hallo") assert scene is not None assert scene.domain == "scene" @@ -81,12 +86,34 @@ async def test_create_service(hass, caplog): "create", { "scene_id": "hallo", + "entities": {"light.kitchen_light": {"state": "on", "brightness": 100}}, + }, + blocking=True, + ) + await hass.async_block_till_done() + + scene = hass.states.get("scene.hallo") + assert scene is not None + assert scene.domain == "scene" + assert scene.name == "hallo" + assert scene.state == "scening" + assert scene.attributes.get("entity_id") == ["light.kitchen_light"] + + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo_2", "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, }, blocking=True, ) - await hass.async_block_till_done() - assert "The scene scene.hallo already exists" in caplog.text - assert hass.states.get("scene.hallo") is not None - assert hass.states.get("scene.hallo_2") is None + + assert "The scene scene.hallo_2 already exists" in caplog.text + scene = hass.states.get("scene.hallo_2") + assert scene is not None + assert scene.domain == "scene" + assert scene.name == "hallo_2" + assert scene.state == "scening" + assert scene.attributes.get("entity_id") == ["light.kitchen"] From 804b6bbc0e8fa9fa587c923855be385f7d276b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 5 Nov 2019 07:21:52 +0200 Subject: [PATCH 177/306] Reduce test requirements duplication, sync flake8 and related (#28538) * Generate pre-commit test dependencies instead of duplicating * Upgrade/sync to flake8 3.7.9, flake8-docstrings 1.5.0, and pydocstyle 4.0.1 https://flake8.readthedocs.io/en/latest/release-notes/3.7.9.html https://gitlab.com/pycqa/flake8-docstrings/blob/1.4.0/HISTORY.rst https://gitlab.com/pycqa/flake8-docstrings/blob/1.5.0/HISTORY.rst http://www.pydocstyle.org/en/4.0.1/release_notes.html * Include requirements_test.txt from *_all.txt instead of copying --- .pre-commit-config-all.yaml | 6 +++--- .pre-commit-config.yaml | 6 +++--- requirements_test.txt | 6 +----- requirements_test_all.txt | 27 +++------------------------ requirements_test_pre_commit.txt | 6 ++++++ script/gen_requirements_all.py | 31 +++++++++++++++++++++++++++---- 6 files changed, 43 insertions(+), 39 deletions(-) create mode 100644 requirements_test_pre_commit.txt diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index 98829e25fc3..3910835ae9d 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -19,12 +19,12 @@ repos: - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.8 + rev: 3.7.9 hooks: - id: flake8 additional_dependencies: - - flake8-docstrings==1.3.1 - - pydocstyle==4.0.0 + - flake8-docstrings==1.5.0 + - pydocstyle==4.0.1 files: ^(homeassistant|script|tests)/.+\.py$ # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4beff14965b..3220ac84866 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,10 +15,10 @@ repos: - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.8 + rev: 3.7.9 hooks: - id: flake8 additional_dependencies: - - flake8-docstrings==1.3.1 - - pydocstyle==4.0.0 + - flake8-docstrings==1.5.0 + - pydocstyle==4.0.1 files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/requirements_test.txt b/requirements_test.txt index 06a2ef1621d..33fab3d6c6a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,16 +2,12 @@ # make new things fail. Manually update these pins when pulling in a # new version -# When updating this file, update .pre-commit-config*.yaml too +-r requirements_test_pre_commit.txt asynctest==0.13.0 -black==19.10b0 codecov==2.0.15 -flake8-docstrings==1.5.0 -flake8==3.7.8 mock-open==1.3.1 mypy==0.740 pre-commit==1.20.0 -pydocstyle==4.0.1 pylint==2.4.3 astroid==2.3.2 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9395c0cefad..2a9b30d3e4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1,28 +1,7 @@ -# Home Assistant test -# linters such as flake8 and pylint should be pinned, as new releases -# make new things fail. Manually update these pins when pulling in a -# new version - -# When updating this file, update .pre-commit-config*.yaml too -asynctest==0.13.0 -black==19.10b0 -codecov==2.0.15 -flake8-docstrings==1.5.0 -flake8==3.7.8 -mock-open==1.3.1 -mypy==0.740 -pre-commit==1.20.0 -pydocstyle==4.0.1 -pylint==2.4.3 -astroid==2.3.2 -pytest-aiohttp==0.3.0 -pytest-cov==2.8.1 -pytest-sugar==0.9.2 -pytest-timeout==1.3.3 -pytest==5.2.2 -requests_mock==1.7.0 -responses==0.10.6 +# Home Assistant tests, full dependency set +# Automatically generated by gen_requirements_all.py, do not edit +-r requirements_test.txt # homeassistant.components.homekit HAP-python==2.6.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt new file mode 100644 index 00000000000..29380ca7cd2 --- /dev/null +++ b/requirements_test_pre_commit.txt @@ -0,0 +1,6 @@ +# Automatically generated from .pre-commit-config-all.yaml by gen_requirements_all.py, do not edit + +black==19.10b0 +flake8-docstrings==1.5.0 +flake8==3.7.9 +pydocstyle==4.0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 930ffa11b5f..9bbe7d379ec 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,6 +8,8 @@ import pkgutil import re import sys +from homeassistant.util.yaml.loader import load_yaml + from script.hassfest.model import Integration COMMENT_REQUIREMENTS = ( @@ -225,10 +227,11 @@ def requirements_all_output(reqs): def requirements_test_output(reqs): """Generate output for test_requirements.""" output = [] - output.append("# Home Assistant test") - output.append("\n") - output.append(Path("requirements_test.txt").read_text()) - output.append("\n") + output.append("# Home Assistant tests, full dependency set\n") + output.append( + f"# Automatically generated by {Path(__file__).name}, do not edit\n\n" + ) + output.append("-r requirements_test.txt\n") filtered = { requirement: modules @@ -246,6 +249,24 @@ def requirements_test_output(reqs): return "".join(output) +def requirements_pre_commit_output(): + """Generate output for pre-commit dependencies.""" + source = ".pre-commit-config-all.yaml" + pre_commit_conf = load_yaml(source) + reqs = [] + for repo in (x for x in pre_commit_conf["repos"] if x.get("rev")): + for hook in repo["hooks"]: + reqs.append(f"{hook['id']}=={repo['rev']}") + reqs.extend(x for x in hook.get("additional_dependencies", ())) + output = [ + f"# Automatically generated " + f"from {source} by {Path(__file__).name}, do not edit", + "", + ] + output.extend(sorted(reqs)) + return "\n".join(output) + "\n" + + def gather_constraints(): """Construct output for constraint file.""" return ( @@ -285,10 +306,12 @@ def main(validate): reqs_file = requirements_all_output(data) reqs_test_file = requirements_test_output(data) + reqs_pre_commit_file = requirements_pre_commit_output() constraints = gather_constraints() files = ( ("requirements_all.txt", reqs_file), + ("requirements_test_pre_commit.txt", reqs_pre_commit_file), ("requirements_test_all.txt", reqs_test_file), ("homeassistant/package_constraints.txt", constraints), ) From 11efb2c2eb1e235fc9aa5f3e6cc6dab833877b21 Mon Sep 17 00:00:00 2001 From: Zach Date: Tue, 5 Nov 2019 05:43:36 -0500 Subject: [PATCH 178/306] Avoid drawing image_processing font text inside the bow line (#27796) * Adjust font text such that it won't be drawn inside the bow line in image_processing.draw_box * Adjust font_height after actually counting the pixels * Thinned out line_width and adjusted font size --- homeassistant/components/image_processing/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index e9621fe6bbe..4c90441e7f0 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -85,7 +85,8 @@ def draw_box( the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). """ - line_width = 5 + line_width = 3 + font_height = 8 y_min, x_min, y_max, x_max = box (left, right, top, bottom) = ( x_min * img_width, @@ -99,7 +100,9 @@ def draw_box( fill=color, ) if text: - draw.text((left + line_width, abs(top - line_width)), text, fill=color) + draw.text( + (left + line_width, abs(top - line_width - font_height)), text, fill=color + ) async def async_setup(hass, config): From a43095b2b5f13321c139312bf733864ab1644c4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 6 Nov 2019 02:24:20 +1300 Subject: [PATCH 179/306] Add override switch for juicenet (#28049) * Add override switch for juicenet * Update generated files * Update indentation * Fix indentation * Remove unnecessary else statement * Update homeassistant/components/juicenet/switch.py Co-Authored-By: Fabian Affolter * Update homeassistant/components/juicenet/switch.py Co-Authored-By: Fabian Affolter * Remove state property * Change string formatting * Bump juicenet package version again --- CODEOWNERS | 1 + homeassistant/components/juicenet/__init__.py | 6 ++- .../components/juicenet/manifest.json | 6 ++- homeassistant/components/juicenet/switch.py | 45 +++++++++++++++++++ requirements_all.txt | 2 +- 5 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/juicenet/switch.py diff --git a/CODEOWNERS b/CODEOWNERS index ce170df3602..ceb58f370d4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -156,6 +156,7 @@ homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi +homeassistant/components/juicenet/* @jesserockz homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 207dac7836a..55bf91ac398 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -18,6 +18,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +JUICENET_COMPONENTS = ["sensor", "switch"] + def setup(hass, config): """Set up the Juicenet component.""" @@ -26,7 +28,9 @@ def setup(hass, config): access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) hass.data[DOMAIN]["api"] = pyjuicenet.Api(access_token) - discovery.load_platform(hass, "sensor", DOMAIN, {}, config) + for component in JUICENET_COMPONENTS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + return True diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 1ef84b74502..076567573c7 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -3,8 +3,10 @@ "name": "Juicenet", "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": [ - "python-juicenet==0.0.5" + "python-juicenet==0.1.5" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@jesserockz" + ] } diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py new file mode 100644 index 00000000000..30bb5b22814 --- /dev/null +++ b/homeassistant/components/juicenet/switch.py @@ -0,0 +1,45 @@ +"""Support for monitoring juicenet/juicepoint/juicebox based EVSE switches.""" +import logging + +from homeassistant.components.switch import SwitchDevice + +from . import DOMAIN, JuicenetDevice + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Juicenet switch.""" + api = hass.data[DOMAIN]["api"] + + devs = [] + for device in api.get_devices(): + devs.append(JuicenetChargeNowSwitch(device, hass)) + + add_entities(devs) + + +class JuicenetChargeNowSwitch(JuicenetDevice, SwitchDevice): + """Implementation of a Juicenet switch.""" + + def __init__(self, device, hass): + """Initialise the switch.""" + super().__init__(device, "charge_now", hass) + + @property + def name(self): + """Return the name of the device.""" + return f"{self.device.name()} Charge Now" + + @property + def is_on(self): + """Return true if switch is on.""" + return self.device.getOverrideTime() != 0 + + def turn_on(self, **kwargs): + """Charge now.""" + self.device.setOverride(True) + + def turn_off(self, **kwargs): + """Don't charge now.""" + self.device.setOverride(False) diff --git a/requirements_all.txt b/requirements_all.txt index 282d04ac77c..99c562870bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1537,7 +1537,7 @@ python-izone==1.1.1 python-join-api==0.0.4 # homeassistant.components.juicenet -python-juicenet==0.0.5 +python-juicenet==0.1.5 # homeassistant.components.lirc # python-lirc==1.2.3 From 136f1f7fe9b1be4b2c3732a007081b3465cb81a6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 5 Nov 2019 15:04:19 +0100 Subject: [PATCH 180/306] Move imports in samsungtv component (#27775) * Move imports in samsungtv component * Fix tests * Fix review 1 * Fix review 2 * wakeonlan is a module --- .../components/samsungtv/media_player.py | 24 +++++------ .../components/samsungtv/test_media_player.py | 42 +++++++++++-------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 94e9131ed32..aa6e3ae62d1 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,7 +3,9 @@ import asyncio from datetime import timedelta import socket +from samsungctl import exceptions as samsung_exceptions, Remote as SamsungRemote import voluptuous as vol +import wakeonlan from homeassistant.components.media_player import ( MediaPlayerDevice, @@ -113,17 +115,11 @@ class SamsungTVDevice(MediaPlayerDevice): def __init__(self, host, port, name, timeout, mac, uuid): """Initialize the Samsung device.""" - from samsungctl import exceptions - from samsungctl import Remote - import wakeonlan # Save a reference to the imported classes - self._exceptions_class = exceptions - self._remote_class = Remote self._name = name self._mac = mac self._uuid = uuid - self._wol = wakeonlan # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -163,13 +159,13 @@ class SamsungTVDevice(MediaPlayerDevice): try: self._config["method"] = method LOGGER.debug("Try config: %s", self._config) - self._remote = self._remote_class(self._config.copy()) + self._remote = SamsungRemote(self._config.copy()) self._state = STATE_ON LOGGER.debug("Found working config: %s", self._config) break except ( - self._exceptions_class.UnhandledResponse, - self._exceptions_class.AccessDenied, + samsung_exceptions.UnhandledResponse, + samsung_exceptions.AccessDenied, ): # We got a response so it's working. self._state = STATE_ON @@ -189,7 +185,7 @@ class SamsungTVDevice(MediaPlayerDevice): if self._remote is None: # We need to create a new instance to reconnect. - self._remote = self._remote_class(self._config.copy()) + self._remote = SamsungRemote(self._config.copy()) return self._remote @@ -205,7 +201,7 @@ class SamsungTVDevice(MediaPlayerDevice): try: self.get_remote().control(key) break - except (self._exceptions_class.ConnectionClosed, BrokenPipeError): + except (samsung_exceptions.ConnectionClosed, BrokenPipeError): # BrokenPipe can occur when the commands is sent to fast self._remote = None self._state = STATE_ON @@ -213,8 +209,8 @@ class SamsungTVDevice(MediaPlayerDevice): # Auto-detect could not find working config yet pass except ( - self._exceptions_class.UnhandledResponse, - self._exceptions_class.AccessDenied, + samsung_exceptions.UnhandledResponse, + samsung_exceptions.AccessDenied, ): # We got a response so it's on. self._state = STATE_ON @@ -343,7 +339,7 @@ class SamsungTVDevice(MediaPlayerDevice): def turn_on(self): """Turn the media player on.""" if self._mac: - self._wol.send_magic_packet(self._mac) + wakeonlan.send_magic_packet(self._mac) else: self.send_key("KEY_POWERON") diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index deb39b4077f..2b5e377c617 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,12 +1,13 @@ -"""Tests for samsungtv Components.""" +"""Tests for samsungtv component.""" import asyncio -from asynctest import mock +from unittest.mock import call, patch from datetime import timedelta + import logging +from asynctest import mock import pytest from samsungctl import exceptions -from tests.common import MockDependency, async_fire_time_changed -from unittest.mock import call, patch +from tests.common import async_fire_time_changed from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( @@ -62,7 +63,7 @@ MOCK_CONFIG = { CONF_NAME: "fake", CONF_PORT: 8001, CONF_TIMEOUT: 10, - CONF_MAC: "fake", + CONF_MAC: "38:f9:d3:82:b4:f1", } } @@ -125,7 +126,9 @@ AUTODETECT_LEGACY = { @pytest.fixture(name="remote") def remote_fixture(): """Patch the samsungctl Remote.""" - with patch("samsungctl.Remote") as remote_class, patch( + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote_class, patch( "homeassistant.components.samsungtv.media_player.socket" ) as socket_class: remote = mock.Mock() @@ -138,8 +141,10 @@ def remote_fixture(): @pytest.fixture(name="wakeonlan") def wakeonlan_fixture(): """Patch the wakeonlan Remote.""" - with MockDependency("wakeonlan") as wakeonlan: - yield wakeonlan + with patch( + "homeassistant.components.samsungtv.media_player.wakeonlan" + ) as wakeonlan_module: + yield wakeonlan_module @pytest.fixture @@ -249,9 +254,9 @@ async def test_send_key(hass, remote, wakeonlan): async def test_send_key_autodetect_websocket(hass, remote): """Test for send key with autodetection of protocol.""" - with patch("samsungctl.Remote") as remote, patch( - "homeassistant.components.samsungtv.media_player.socket" - ): + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): await setup_samsungtv(hass, MOCK_CONFIG_AUTO) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True @@ -266,7 +271,8 @@ async def test_send_key_autodetect_websocket_exception(hass, caplog): """Test for send key with autodetection of protocol.""" caplog.set_level(logging.DEBUG) with patch( - "samsungctl.Remote", side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT] + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT], ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): await setup_samsungtv(hass, MOCK_CONFIG_AUTO) assert await hass.services.async_call( @@ -287,7 +293,8 @@ async def test_send_key_autodetect_websocket_exception(hass, caplog): async def test_send_key_autodetect_legacy(hass, remote): """Test for send key with autodetection of protocol.""" with patch( - "samsungctl.Remote", side_effect=[OSError("Boom"), mock.DEFAULT] + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=[OSError("Boom"), mock.DEFAULT], ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): await setup_samsungtv(hass, MOCK_CONFIG_AUTO) assert await hass.services.async_call( @@ -304,9 +311,10 @@ async def test_send_key_autodetect_legacy(hass, remote): async def test_send_key_autodetect_none(hass, remote): """Test for send key with autodetection of protocol.""" - with patch("samsungctl.Remote", side_effect=OSError("Boom")) as remote, patch( - "homeassistant.components.samsungtv.media_player.socket" - ): + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=OSError("Boom"), + ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): await setup_samsungtv(hass, MOCK_CONFIG_AUTO) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True @@ -557,7 +565,7 @@ async def test_turn_on_with_mac(hass, remote, wakeonlan): ) # key and update called assert wakeonlan.send_magic_packet.call_count == 1 - assert wakeonlan.send_magic_packet.call_args_list == [call("fake")] + assert wakeonlan.send_magic_packet.call_args_list == [call("38:f9:d3:82:b4:f1")] async def test_turn_on_without_mac(hass, remote): From 7b86f0f9265dc8cdcf7ed92c0db7340604134569 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 5 Nov 2019 15:43:50 +0100 Subject: [PATCH 181/306] Add deprecated attributes to light.reproduce_state (#28557) * Add deprecated attributes to light.reproduce_state * Add blank line * fix minor bug * Typo --- .../components/light/reproduce_state.py | 42 +++++++++++- .../components/light/test_reproduce_state.py | 66 +++++++++++++++++-- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index ae618f7a8ef..c84b3627bed 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -17,10 +17,16 @@ from homeassistant.helpers.typing import HomeAssistantType from . import ( DOMAIN, ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, + ATTR_FLASH, ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_PROFILE, ATTR_RGB_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, ) @@ -28,8 +34,36 @@ from . import ( _LOGGER = logging.getLogger(__name__) VALID_STATES = {STATE_ON, STATE_OFF} -ATTR_GROUP = [ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_WHITE_VALUE] -COLOR_GROUP = [ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR] + +ATTR_GROUP = [ + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_WHITE_VALUE, + ATTR_TRANSITION, +] + +COLOR_GROUP = [ + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_PROFILE, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, +] + +DEPRECATED_GROUP = [ + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, + ATTR_FLASH, + ATTR_KELVIN, + ATTR_PROFILE, + ATTR_TRANSITION, +] + +DEPRECATION_WARNING = "The use of other attributes than device state attributes is deprecated and will be removed in a future release. Read the logs for further details: https://www.home-assistant.io/integrations/scene/" async def _async_reproduce_state( @@ -48,6 +82,10 @@ async def _async_reproduce_state( ) return + # Warn if deprecated attributes are used + if any(attr in DEPRECATED_GROUP for attr in state.attributes): + _LOGGER.warning(DEPRECATION_WARNING) + # Return if we are already at the right state. if cur_state.state == state.state and all( check_attr_equal(cur_state.attributes, state.attributes, attr) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 92790890a4c..250a0fe26a8 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -1,13 +1,19 @@ """Test reproduce state for Light.""" +from homeassistant.components.light.reproduce_state import DEPRECATION_WARNING from homeassistant.core import State from tests.common import async_mock_service VALID_BRIGHTNESS = {"brightness": 180} VALID_WHITE_VALUE = {"white_value": 200} +VALID_FLASH = {"flash": "short"} VALID_EFFECT = {"effect": "random"} +VALID_TRANSITION = {"transition": 15} +VALID_COLOR_NAME = {"color_name": "red"} VALID_COLOR_TEMP = {"color_temp": 240} VALID_HS_COLOR = {"hs_color": (345, 75)} +VALID_KELVIN = {"kelvin": 4000} +VALID_PROFILE = {"profile": "relax"} VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)} VALID_XY_COLOR = {"xy_color": (0.59, 0.274)} @@ -17,9 +23,14 @@ async def test_reproducing_states(hass, caplog): hass.states.async_set("light.entity_off", "off", {}) hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS) hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE) + hass.states.async_set("light.entity_flash", "on", VALID_FLASH) hass.states.async_set("light.entity_effect", "on", VALID_EFFECT) + hass.states.async_set("light.entity_trans", "on", VALID_TRANSITION) + hass.states.async_set("light.entity_name", "on", VALID_COLOR_NAME) hass.states.async_set("light.entity_temp", "on", VALID_COLOR_TEMP) hass.states.async_set("light.entity_hs", "on", VALID_HS_COLOR) + hass.states.async_set("light.entity_kelvin", "on", VALID_KELVIN) + hass.states.async_set("light.entity_profile", "on", VALID_PROFILE) hass.states.async_set("light.entity_rgb", "on", VALID_RGB_COLOR) hass.states.async_set("light.entity_xy", "on", VALID_XY_COLOR) @@ -32,9 +43,14 @@ async def test_reproducing_states(hass, caplog): State("light.entity_off", "off"), State("light.entity_bright", "on", VALID_BRIGHTNESS), State("light.entity_white", "on", VALID_WHITE_VALUE), + State("light.entity_flash", "on", VALID_FLASH), State("light.entity_effect", "on", VALID_EFFECT), + State("light.entity_trans", "on", VALID_TRANSITION), + State("light.entity_name", "on", VALID_COLOR_NAME), State("light.entity_temp", "on", VALID_COLOR_TEMP), State("light.entity_hs", "on", VALID_HS_COLOR), + State("light.entity_kelvin", "on", VALID_KELVIN), + State("light.entity_profile", "on", VALID_PROFILE), State("light.entity_rgb", "on", VALID_RGB_COLOR), State("light.entity_xy", "on", VALID_XY_COLOR), ], @@ -59,16 +75,21 @@ async def test_reproducing_states(hass, caplog): State("light.entity_xy", "off"), State("light.entity_off", "on", VALID_BRIGHTNESS), State("light.entity_bright", "on", VALID_WHITE_VALUE), - State("light.entity_white", "on", VALID_EFFECT), - State("light.entity_effect", "on", VALID_COLOR_TEMP), + State("light.entity_white", "on", VALID_FLASH), + State("light.entity_flash", "on", VALID_EFFECT), + State("light.entity_effect", "on", VALID_TRANSITION), + State("light.entity_trans", "on", VALID_COLOR_NAME), + State("light.entity_name", "on", VALID_COLOR_TEMP), State("light.entity_temp", "on", VALID_HS_COLOR), - State("light.entity_hs", "on", VALID_RGB_COLOR), + State("light.entity_hs", "on", VALID_KELVIN), + State("light.entity_kelvin", "on", VALID_PROFILE), + State("light.entity_profile", "on", VALID_RGB_COLOR), State("light.entity_rgb", "on", VALID_XY_COLOR), ], blocking=True, ) - assert len(turn_on_calls) == 7 + assert len(turn_on_calls) == 12 expected_calls = [] @@ -80,22 +101,42 @@ async def test_reproducing_states(hass, caplog): expected_bright["entity_id"] = "light.entity_bright" expected_calls.append(expected_bright) - expected_white = VALID_EFFECT + expected_white = VALID_FLASH expected_white["entity_id"] = "light.entity_white" expected_calls.append(expected_white) - expected_effect = VALID_COLOR_TEMP + expected_flash = VALID_EFFECT + expected_flash["entity_id"] = "light.entity_flash" + expected_calls.append(expected_flash) + + expected_effect = VALID_TRANSITION expected_effect["entity_id"] = "light.entity_effect" expected_calls.append(expected_effect) + expected_trans = VALID_COLOR_NAME + expected_trans["entity_id"] = "light.entity_trans" + expected_calls.append(expected_trans) + + expected_name = VALID_COLOR_TEMP + expected_name["entity_id"] = "light.entity_name" + expected_calls.append(expected_name) + expected_temp = VALID_HS_COLOR expected_temp["entity_id"] = "light.entity_temp" expected_calls.append(expected_temp) - expected_hs = VALID_RGB_COLOR + expected_hs = VALID_KELVIN expected_hs["entity_id"] = "light.entity_hs" expected_calls.append(expected_hs) + expected_kelvin = VALID_PROFILE + expected_kelvin["entity_id"] = "light.entity_kelvin" + expected_calls.append(expected_kelvin) + + expected_profile = VALID_RGB_COLOR + expected_profile["entity_id"] = "light.entity_profile" + expected_calls.append(expected_profile) + expected_rgb = VALID_XY_COLOR expected_rgb["entity_id"] = "light.entity_rgb" expected_calls.append(expected_rgb) @@ -115,3 +156,14 @@ async def test_reproducing_states(hass, caplog): assert len(turn_off_calls) == 1 assert turn_off_calls[0].domain == "light" assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"} + + +async def test_deprecation_warning(hass, caplog): + """Test deprecation warning.""" + hass.states.async_set("light.entity_off", "off", {}) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + await hass.helpers.state.async_reproduce_state( + [State("light.entity_off", "on", {"brightness_pct": 80})], blocking=True + ) + assert len(turn_on_calls) == 1 + assert DEPRECATION_WARNING in caplog.text From 1e398a89669599a3c34e8b79df1301998d6e3a08 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 5 Nov 2019 21:12:29 +0100 Subject: [PATCH 182/306] Try fix tests (#28470) --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index e80f4b8d0ba..bcd72882df5 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -127,7 +127,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 -n 2 --dist loadfile -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar tests script/check_dirty displayName: 'Run pytest for python $(python.container)' condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) From 10247f6799e75a9e4f8ee9a75d0f37c77b59d4b6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 5 Nov 2019 21:38:30 +0100 Subject: [PATCH 183/306] Fix dev dockerfile --- Dockerfile.dev | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index eb76fe5b16b..fa90a84fc1e 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -24,9 +24,9 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ WORKDIR /workspaces # Install Python dependencies from requirements -COPY requirements_test.txt homeassistant/package_constraints.txt ./ +COPY requirements_test.txt requirements_test_pre_commit.txt homeassistant/package_constraints.txt ./ RUN pip3 install -r requirements_test.txt -c package_constraints.txt \ - && rm -f requirements_test.txt package_constraints.txt + && rm -f requirements_test.txt package_constraints.txt requirements_test_pre_commit.txt # Set the default shell to bash instead of sh ENV SHELL /bin/bash From 925e26b0618247b6eccc5a0fedc6afa2ed9d2a58 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 5 Nov 2019 21:58:35 +0100 Subject: [PATCH 184/306] Update azure-pipelines-ci.yml --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index bcd72882df5..37473b92620 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -135,7 +135,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 -n 2 --dist loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' From 005a1b2713c92d312fa8c267e25cf4882f5a4a95 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 5 Nov 2019 22:39:15 +0100 Subject: [PATCH 185/306] Add additional support over NC (#28527) * Add voice support over NC * Add disocery support for TTS / STT * fix cloud TTS discovery * Fix dev config * Fix discovery * Bump hass-nabucasa 0.25 * Add channel support * Fix lint * Update homeassistant/components/cloud/__init__.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen * bump hass-nabucasa * Update tts.py * fix lint --- homeassistant/components/amazon_polly/tts.py | 2 +- homeassistant/components/baidu/tts.py | 2 +- homeassistant/components/cloud/__init__.py | 19 +++- homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloud/stt.py | 106 ++++++++++++++++++ homeassistant/components/cloud/tts.py | 81 +++++++++++++ homeassistant/components/demo/stt.py | 10 +- homeassistant/components/demo/tts.py | 2 +- homeassistant/components/google_cloud/tts.py | 2 +- .../components/google_translate/tts.py | 2 +- homeassistant/components/marytts/tts.py | 2 +- homeassistant/components/microsoft/tts.py | 2 +- homeassistant/components/picotts/tts.py | 2 +- homeassistant/components/stt/__init__.py | 35 ++++-- homeassistant/components/stt/const.py | 11 +- homeassistant/components/tts/__init__.py | 21 +++- homeassistant/components/voicerss/tts.py | 2 +- homeassistant/components/watson_tts/tts.py | 2 +- homeassistant/components/yandextts/tts.py | 2 +- homeassistant/package_constraints.txt | 2 +- pylintrc | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/demo/test_stt.py | 5 +- 25 files changed, 282 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/cloud/stt.py create mode 100644 homeassistant/components/cloud/tts.py diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 3acfd472320..3d05236935f 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -145,7 +145,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Amazon Polly speech component.""" output_format = config.get(CONF_OUTPUT_FORMAT) sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format]) diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index 8d753753e5a..4208750b7fc 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -52,7 +52,7 @@ _OPTIONS = { SUPPORTED_OPTIONS = [CONF_PERSON, CONF_PITCH, CONF_SPEED, CONF_VOLUME] -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Baidu TTS component.""" return BaiduTTSProvider(hass, config) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 2d5a2c8b448..763f6214185 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -23,6 +23,7 @@ from homeassistant.util.aiohttp import MockRequest from . import account_link, http_api from .client import CloudClient from .const import ( + CONF_ACCOUNT_LINK_URL, CONF_ACME_DIRECTORY_SERVER, CONF_ALEXA, CONF_ALEXA_ACCESS_TOKEN_URL, @@ -38,7 +39,7 @@ from .const import ( CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, - CONF_ACCOUNT_LINK_URL, + CONF_VOICE_API_URL, DOMAIN, MODE_DEV, MODE_PROD, @@ -103,6 +104,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(), vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(), vol.Optional(CONF_ACCOUNT_LINK_URL): vol.Url(), + vol.Optional(CONF_VOICE_API_URL): vol.Url(), } ) }, @@ -230,21 +232,28 @@ async def async_setup(hass, config): DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler ) - loaded_binary_sensor = False + loaded = False async def _on_connect(): """Discover RemoteUI binary sensor.""" - nonlocal loaded_binary_sensor + nonlocal loaded - if loaded_binary_sensor: + # Prevent multiple discovery + if loaded: return + loaded = True - loaded_binary_sensor = True hass.async_create_task( hass.helpers.discovery.async_load_platform( "binary_sensor", DOMAIN, {}, config ) ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("stt", DOMAIN, {}, config) + ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("tts", DOMAIN, {}, config) + ) cloud.iot.register_on_connect(_on_connect) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 262f84a85e6..9a2dccf8d7c 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -38,6 +38,7 @@ CONF_ACME_DIRECTORY_SERVER = "acme_directory_server" CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url" CONF_ACCOUNT_LINK_URL = "account_link_url" +CONF_VOICE_API_URL = "voice_api_url" MODE_DEV = "development" MODE_PROD = "production" diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 9e9b77287ae..2876ff11b7e 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.23"], + "requirements": ["hass-nabucasa==0.26"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py new file mode 100644 index 00000000000..acca36afae9 --- /dev/null +++ b/homeassistant/components/cloud/stt.py @@ -0,0 +1,106 @@ +"""Support for the cloud for speech to text service.""" +from typing import List + +from aiohttp import StreamReader +from hass_nabucasa import Cloud +from hass_nabucasa.voice import VoiceError + +from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult +from homeassistant.components.stt.const import ( + AudioBitRates, + AudioChannels, + AudioCodecs, + AudioFormats, + AudioSampleRates, + SpeechResultState, +) + +from .const import DOMAIN + +SUPPORT_LANGUAGES = [ + "da-DK", + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-US", + "es-ES", + "fi-FI", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + "nl-NL", + "pl-PL", + "pt-PT", + "ru-RU", + "sv-SE", + "th-TH", + "zh-CN", + "zh-HK", +] + + +async def async_get_engine(hass, config, discovery_info=None): + """Set up Cloud speech component.""" + cloud: Cloud = hass.data[DOMAIN] + + return CloudProvider(cloud) + + +class CloudProvider(Provider): + """NabuCasa speech API provider.""" + + def __init__(self, cloud: Cloud) -> None: + """Hass NabuCasa Speech to text.""" + self.cloud = cloud + + @property + def supported_languages(self) -> List[str]: + """Return a list of supported languages.""" + return SUPPORT_LANGUAGES + + @property + def supported_formats(self) -> List[AudioFormats]: + """Return a list of supported formats.""" + return [AudioFormats.WAV, AudioFormats.OGG] + + @property + def supported_codecs(self) -> List[AudioCodecs]: + """Return a list of supported codecs.""" + return [AudioCodecs.PCM, AudioCodecs.OPUS] + + @property + def supported_bit_rates(self) -> List[AudioBitRates]: + """Return a list of supported bitrates.""" + return [AudioBitRates.BITRATE_16] + + @property + def supported_sample_rates(self) -> List[AudioSampleRates]: + """Return a list of supported samplerates.""" + return [AudioSampleRates.SAMPLERATE_16000] + + @property + def supported_channels(self) -> List[AudioChannels]: + """Return a list of supported channels.""" + return [AudioChannels.CHANNEL_MONO] + + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: StreamReader + ) -> SpeechResult: + """Process an audio stream to STT service.""" + content = f"audio/{metadata.format!s}; codecs=audio/{metadata.codec!s}; samplerate=16000" + + # Process STT + try: + result = await self.cloud.voice.process_stt( + stream, content, metadata.language + ) + except VoiceError: + return SpeechResult(None, SpeechResultState.ERROR) + + # Return Speech as Text + return SpeechResult( + result.text, + SpeechResultState.SUCCESS if result.success else SpeechResultState.ERROR, + ) diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py new file mode 100644 index 00000000000..338b97d2bd9 --- /dev/null +++ b/homeassistant/components/cloud/tts.py @@ -0,0 +1,81 @@ +"""Support for the cloud for text to speech service.""" + +from hass_nabucasa.voice import VoiceError +from hass_nabucasa import Cloud +import voluptuous as vol + +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider + +from .const import DOMAIN + +CONF_GENDER = "gender" + +SUPPORT_LANGUAGES = ["en-US", "de-DE", "es-ES"] +SUPPORT_GENDER = ["male", "female"] + +DEFAULT_LANG = "en-US" +DEFAULT_GENDER = "female" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), + vol.Optional(CONF_GENDER, default=DEFAULT_GENDER): vol.In(SUPPORT_GENDER), + } +) + + +async def async_get_engine(hass, config, discovery_info=None): + """Set up Cloud speech component.""" + cloud: Cloud = hass.data[DOMAIN] + + if discovery_info is not None: + language = DEFAULT_LANG + gender = DEFAULT_GENDER + else: + language = config[CONF_LANG] + gender = config[CONF_GENDER] + + return CloudProvider(cloud, language, gender) + + +class CloudProvider(Provider): + """NabuCasa Cloud speech API provider.""" + + def __init__(self, cloud: Cloud, language: str, gender: str): + """Initialize cloud provider.""" + self.cloud = cloud + self.name = "Cloud" + self._language = language + self._gender = gender + + @property + def default_language(self): + """Return the default language.""" + return self._language + + @property + def supported_languages(self): + """Return list of supported languages.""" + return SUPPORT_LANGUAGES + + @property + def supported_options(self): + """Return list of supported options like voice, emotion.""" + return [CONF_GENDER] + + @property + def default_options(self): + """Return a dict include default options.""" + return {CONF_GENDER: self._gender} + + async def async_get_tts_audio(self, message, language, options=None): + """Load TTS from NabuCasa Cloud.""" + # Process TTS + try: + data = await self.cloud.voice.process_tts( + message, language, gender=options[CONF_GENDER] + ) + except VoiceError: + return (None, None) + + return ("mp3", data) diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index fdd89dc4c9c..e0367fad6a9 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -6,16 +6,17 @@ from aiohttp import StreamReader from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult from homeassistant.components.stt.const import ( AudioBitRates, + AudioChannels, + AudioCodecs, AudioFormats, AudioSampleRates, - AudioCodecs, SpeechResultState, ) SUPPORT_LANGUAGES = ["en", "de"] -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up Demo speech component.""" return DemoProvider() @@ -48,6 +49,11 @@ class DemoProvider(Provider): """Return a list of supported sample rates.""" return [AudioSampleRates.SAMPLERATE_16000, AudioSampleRates.SAMPLERATE_44100] + @property + def supported_channels(self) -> List[AudioChannels]: + """Return a list of supported channels.""" + return [AudioChannels.CHANNEL_STEREO] + async def async_process_audio_stream( self, metadata: SpeechMetadata, stream: StreamReader ) -> SpeechResult: diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index 441b0cc0b3c..27c9015533e 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -14,7 +14,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Demo speech component.""" return DemoProvider(config[CONF_LANG]) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index cf064b3bfa7..942ee0a4e48 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -122,7 +122,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up Google Cloud TTS component.""" key_file = config.get(CONF_KEY_FILE) if key_file: diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index cdf6dbd402e..3add45b8cb8 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -81,7 +81,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up Google speech component.""" return GoogleProvider(hass, config[CONF_LANG]) diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index e5088c5b2df..742b5e87661 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up MaryTTS speech component.""" return MaryTTSProvider(hass, config) diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index d214f6648dd..447d2a4d46a 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -94,7 +94,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Microsoft speech component.""" return MicrosoftProvider( config[CONF_API_KEY], diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index d4fe6bc779b..f9c93edb4fc 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Pico speech component.""" if shutil.which("pico2wave") is None: _LOGGER.error("'pico2wave' was not found") diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index b781c4666ae..b39ab88484b 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -7,21 +7,22 @@ from typing import Dict, List, Optional from aiohttp import StreamReader, web from aiohttp.hdrs import istr from aiohttp.web_exceptions import ( + HTTPBadRequest, HTTPNotFound, HTTPUnsupportedMediaType, - HTTPBadRequest, ) import attr from homeassistant.components.http import HomeAssistantView from homeassistant.core import callback -from homeassistant.helpers import config_per_platform +from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform from .const import ( DOMAIN, AudioBitRates, + AudioChannels, AudioCodecs, AudioFormats, AudioSampleRates, @@ -37,14 +38,17 @@ async def async_setup(hass: HomeAssistantType, config): """Set up STT.""" providers = {} - async def async_setup_platform(p_type, p_config, disc_info=None): + async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a TTS platform.""" + if p_config is None: + p_config = {} + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) if platform is None: return try: - provider = await platform.async_get_engine(hass, p_config) + provider = await platform.async_get_engine(hass, p_config, discovery_info) if provider is None: _LOGGER.error("Error setting up platform %s", p_type) return @@ -65,6 +69,13 @@ async def async_setup(hass: HomeAssistantType, config): if setup_tasks: await asyncio.wait(setup_tasks) + # Add discovery support + async def async_platform_discovered(platform, info): + """Handle for discovered platform.""" + await async_setup_platform(platform, discovery_info=info) + + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + hass.http.register_view(SpeechToTextView(providers)) return True @@ -78,13 +89,14 @@ class SpeechMetadata: codec: AudioCodecs = attr.ib() bit_rate: AudioBitRates = attr.ib(converter=int) sample_rate: AudioSampleRates = attr.ib(converter=int) + channel: AudioChannels = attr.ib(converter=int) @attr.s class SpeechResult: """Result of audio Speech.""" - text: str = attr.ib() + text: Optional[str] = attr.ib() result: SpeechResultState = attr.ib() @@ -112,12 +124,17 @@ class Provider(ABC): @property @abstractmethod def supported_bit_rates(self) -> List[AudioBitRates]: - """Return a list of supported bit_rates.""" + """Return a list of supported bit rates.""" @property @abstractmethod def supported_sample_rates(self) -> List[AudioSampleRates]: - """Return a list of supported sample_rates.""" + """Return a list of supported sample rates.""" + + @property + @abstractmethod + def supported_channels(self) -> List[AudioChannels]: + """Return a list of supported channels.""" @abstractmethod async def async_process_audio_stream( @@ -137,6 +154,7 @@ class Provider(ABC): or metadata.codec not in self.supported_codecs or metadata.bit_rate not in self.supported_bit_rates or metadata.sample_rate not in self.supported_sample_rates + or metadata.channel not in self.supported_channels ): return False return True @@ -157,7 +175,7 @@ class SpeechToTextView(HomeAssistantView): def _metadata_from_header(request: web.Request) -> Optional[SpeechMetadata]: """Extract metadata from header. - X-Speech-Content: format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de_de + X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de """ try: data = request.headers[istr("X-Speech-Content")].split(";") @@ -213,5 +231,6 @@ class SpeechToTextView(HomeAssistantView): "codecs": stt_provider.supported_codecs, "sample_rates": stt_provider.supported_sample_rates, "bit_rates": stt_provider.supported_bit_rates, + "channels": stt_provider.supported_channels, } ) diff --git a/homeassistant/components/stt/const.py b/homeassistant/components/stt/const.py index c653bcc3bd5..c111aed82a4 100644 --- a/homeassistant/components/stt/const.py +++ b/homeassistant/components/stt/const.py @@ -19,7 +19,7 @@ class AudioFormats(str, Enum): class AudioBitRates(int, Enum): - """Supported Audio bit_rates.""" + """Supported Audio bit rates.""" BITRATE_8 = 8 BITRATE_16 = 16 @@ -28,7 +28,7 @@ class AudioBitRates(int, Enum): class AudioSampleRates(int, Enum): - """Supported Audio sample_rates.""" + """Supported Audio sample rates.""" SAMPLERATE_8000 = 8000 SAMPLERATE_11000 = 11000 @@ -41,6 +41,13 @@ class AudioSampleRates(int, Enum): SAMPLERATE_48000 = 48000 +class AudioChannels(int, Enum): + """Supported Audio channel.""" + + CHANNEL_MONO = 1 + CHANNEL_STEREO = 2 + + class SpeechResultState(str, Enum): """Result state of speech.""" diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 2ce0e18bee5..d17f64a3a3a 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -25,7 +25,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform +from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform @@ -118,17 +118,24 @@ async def async_setup(hass, config): hass.http.register_view(TextToSpeechView(tts)) hass.http.register_view(TextToSpeechUrlView(tts)) - async def async_setup_platform(p_type, p_config, disc_info=None): + async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a TTS platform.""" + if p_config is None: + p_config = {} + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) if platform is None: return try: if hasattr(platform, "async_get_engine"): - provider = await platform.async_get_engine(hass, p_config) + provider = await platform.async_get_engine( + hass, p_config, discovery_info + ) else: - provider = await hass.async_add_job(platform.get_engine, hass, p_config) + provider = await hass.async_add_job( + platform.get_engine, hass, p_config, discovery_info + ) if provider is None: _LOGGER.error("Error setting up platform %s", p_type) @@ -178,6 +185,12 @@ async def async_setup(hass, config): if setup_tasks: await asyncio.wait(setup_tasks) + async def async_platform_discovered(platform, info): + """Handle for discovered platform.""" + await async_setup_platform(platform, discovery_info=info) + + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + async def async_clear_cache_handle(service): """Handle clear cache service call.""" await tts.async_clear_cache() diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 3eb7baef8eb..9f87dabf94f 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -131,7 +131,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up VoiceRSS TTS component.""" return VoiceRSSProvider(hass, config) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 021767e3d11..40ebd768a31 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -90,7 +90,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up IBM Watson TTS component.""" from ibm_watson import TextToSpeechV1 from ibm_cloud_sdk_core.authenticators import IAMAuthenticator diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index 8541e6560ab..b06df4b7a42 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -79,7 +79,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( SUPPORTED_OPTIONS = [CONF_CODEC, CONF_VOICE, CONF_EMOTION, CONF_SPEED] -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up VoiceRSS speech component.""" return YandexSpeechKitProvider(hass, config) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7a7d8331952..775545c4ff1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 -hass-nabucasa==0.23 +hass-nabucasa==0.26 home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.3 diff --git a/pylintrc b/pylintrc index 4aced384b63..ff47af6087b 100644 --- a/pylintrc +++ b/pylintrc @@ -44,6 +44,7 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, + too-many-boolean-expressions, unnecessary-pass, unused-argument diff --git a/requirements_all.txt b/requirements_all.txt index 99c562870bb..6250c09c921 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.23 +hass-nabucasa==0.26 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a9b30d3e4f..f4db717a631 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.23 +hass-nabucasa==0.26 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index 782d89688c7..5933b976460 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -27,6 +27,7 @@ async def test_demo_settings(hass_client): "sample_rates": [16000, 44100], "formats": ["wav"], "codecs": ["pcm"], + "channels": [2], } @@ -45,7 +46,7 @@ async def test_demo_speech_wrong_metadata(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; language=de" + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; channel=1; language=de" }, data=b"Test", ) @@ -59,7 +60,7 @@ async def test_demo_speech(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; language=de" + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=2; language=de" }, data=b"Test", ) From e69cd271ddbc4ecc03efc87c8ed6dddeb065bb1c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 5 Nov 2019 14:40:12 -0700 Subject: [PATCH 186/306] Bump pytile and re-order imports (#28570) --- homeassistant/components/tile/device_tracker.py | 10 ++-------- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 924fa913d30..1cb88f67c2f 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -2,6 +2,8 @@ import logging from datetime import timedelta +from pytile import async_login +from pytile.errors import SessionExpiredError, TileError import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA @@ -43,8 +45,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Tile scanner.""" - from pytile import async_login - websession = aiohttp_client.async_get_clientsession(hass) config_file = hass.config.path( @@ -89,8 +89,6 @@ class TileScanner: async def async_init(self): """Further initialize connection to the Tile servers.""" - from pytile.errors import TileError - try: await self._client.async_init() except TileError as err: @@ -105,10 +103,6 @@ class TileScanner: async def _async_update(self, now=None): """Update info from Tile.""" - from pytile.errors import SessionExpiredError, TileError - - _LOGGER.debug("Updating Tile data") - try: await self._client.async_init() tiles = await self._client.tiles.all( diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 0dd0b70ef52..801e09fd954 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": [ - "pytile==3.0.0" + "pytile==3.0.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 6250c09c921..e36c92471c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1609,7 +1609,7 @@ python_opendata_transport==0.1.4 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==3.0.0 +pytile==3.0.1 # homeassistant.components.touchline pytouchline==0.7 From 8ab04d5fc75836e12b0bbcedf2a6a68d01c4c97d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 6 Nov 2019 00:31:46 +0000 Subject: [PATCH 187/306] [ci skip] Translation update --- homeassistant/components/auth/.translations/ko.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 6c2e8988d83..be160b185ac 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, From a63e9764961df9b15e07fd8c7e1e10e5c7fc0f99 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Nov 2019 20:33:11 -0800 Subject: [PATCH 188/306] Fix invalid JSON in deconz strings.json --- homeassistant/components/deconz/strings.json | 191 ++++++++++--------- 1 file changed, 96 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 56186feb8b1..a00e10f3e7e 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,101 +1,102 @@ { - "config": { - "title": "deCONZ Zigbee gateway", - "flow_title": "deCONZ Zigbee gateway ({host})", - "step": { - "init": { - "title": "Define deCONZ gateway", - "data": { - "host": "Host", - "port": "Port" - } - }, - "link": { - "title": "Link with deCONZ", - "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button" - }, - "options": { - "title": "Extra configuration options for deCONZ", - "data":{ - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - } - }, - "hassio_confirm": { - "title": "deCONZ Zigbee gateway via Hass.io add-on", - "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the hass.io add-on {addon}?", - "data": { - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - } - } - }, - "error": { - "no_key": "Couldn't get an API key" - }, - "abort": { - "already_configured": "Bridge is already configured", - "already_in_progress": "Config flow for bridge is already in progress.", - "no_bridges": "No deCONZ bridges discovered", - "not_deconz_bridge": "Not a deCONZ bridge", - "one_instance_only": "Component only supports one deCONZ instance", - "updated_instance": "Updated deCONZ instance with new host address" + "config": { + "title": "deCONZ Zigbee gateway", + "flow_title": "deCONZ Zigbee gateway ({host})", + "step": { + "init": { + "title": "Define deCONZ gateway", + "data": { + "host": "Host", + "port": "Port" } - }, - "options": { - "step": { - "deconz_devices": { - "description": "Configure visibility of deCONZ device types", - "data": { - "allow_clip_sensor": "Allow deCONZ CLIP sensors", - "allow_deconz_groups": "Allow deCONZ light groups" - } - } + }, + "link": { + "title": "Link with deCONZ", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors", + "allow_deconz_groups": "Allow importing deCONZ groups" } + }, + "hassio_confirm": { + "title": "deCONZ Zigbee gateway via Hass.io add-on", + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the hass.io add-on {addon}?", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors", + "allow_deconz_groups": "Allow importing deCONZ groups" + } + } }, - "device_automation": { - "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_rotated": "Button rotated \"{subtype}\"", - "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", - "remote_falling": "Device in free fall", - "remote_awakened": "Device awakened", - "remote_moved": "Device moved with \"{subtype}\" up", - "remote_double_tap": "Device \"{subtype}\" double tapped", - "remote_gyro_activated": "Device shaken", - "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", - "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", - "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", - "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", - "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", - "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" - }, - "trigger_subtype": { - "turn_on": "Turn on", - "turn_off": "Turn off", - "dim_up": "Dim up", - "dim_down": "Dim down", - "left": "Left", - "right": "Right", - "open": "Open", - "close": "Close", - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "side_1": "Side 1", - "side_2": "Side 2", - "side_3": "Side 3", - "side_4": "Side 4", - "side_5": "Side 5", - "side_6": "Side 6" + "error": { + "no_key": "Couldn't get an API key" + }, + "abort": { + "already_configured": "Bridge is already configured", + "already_in_progress": "Config flow for bridge is already in progress.", + "no_bridges": "No deCONZ bridges discovered", + "not_deconz_bridge": "Not a deCONZ bridge", + "one_instance_only": "Component only supports one deCONZ instance", + "updated_instance": "Updated deCONZ instance with new host address" } + }, + "options": { + "step": { + "deconz_devices": { + "description": "Configure visibility of deCONZ device types", + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + } + } + } + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", + "remote_falling": "Device in free fall", + "remote_awakened": "Device awakened", + "remote_moved": "Device moved with \"{subtype}\" up", + "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_gyro_activated": "Device shaken", + "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", + "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", + "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", + "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", + "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6" + } + } } From f8712b4d7f71204f6bb8bf50bee5788ee6906fb4 Mon Sep 17 00:00:00 2001 From: Grodesh <4070488+Grodesh@users.noreply.github.com> Date: Wed, 6 Nov 2019 01:24:11 -0500 Subject: [PATCH 189/306] Update nextbus stop tag to accept strings (#27765) --- homeassistant/components/nextbus/sensor.py | 2 +- tests/components/nextbus/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 661eb75b732..7622bd133f0 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -173,7 +173,7 @@ class NextBusDepartureSensor(Entity): """Update sensor with new departures times.""" # Note: using Multi because there is a bug with the single stop impl results = self._client.get_predictions_for_multi_stops( - [{"stop_tag": int(self.stop), "route_tag": self.route}], self.agency + [{"stop_tag": self.stop, "route_tag": self.route}], self.agency ) self._log_debug("Predictions results: %s", results) diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 2aaf622ffc9..d7c1919dff0 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -113,7 +113,7 @@ async def test_verify_valid_state( """Verify all attributes are set from a valid response.""" await assert_setup_sensor(hass, CONFIG_BASIC) mock_nextbus_predictions.assert_called_once_with( - [{"stop_tag": int(VALID_STOP), "route_tag": VALID_ROUTE}], VALID_AGENCY + [{"stop_tag": VALID_STOP, "route_tag": VALID_ROUTE}], VALID_AGENCY ) state = hass.states.get(SENSOR_ID_SHORT) From 438ee991751347262357a47ce5e94ed5bc988695 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 5 Nov 2019 23:02:07 -0800 Subject: [PATCH 190/306] Bump adb-shell to 0.0.8 (#28582) * Bump 'adb-shell' to 0.0.8 * Update requirements_test_all.txt * Update manifest.json --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 9ec993b9f91..c734b525e55 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.7", + "adb-shell==0.0.8", "androidtv==0.0.32" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index e36c92471c3..86400df014c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.7 +adb-shell==0.0.8 # homeassistant.components.adguard adguardhome==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4db717a631..50f4ae5f2a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -29,7 +29,7 @@ YesssSMS==0.4.1 abodepy==0.16.6 # homeassistant.components.androidtv -adb-shell==0.0.7 +adb-shell==0.0.8 # homeassistant.components.adguard adguardhome==0.3.0 From e99bb8f75e02ca49d067220e02151c327571c788 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 6 Nov 2019 02:45:16 -0500 Subject: [PATCH 191/306] Fix Doods error when detection labels are specified in list form (#28574) --- homeassistant/components/doods/image_processing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 02d7ce26f1c..7f2c3fd1d42 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -177,6 +177,7 @@ class Doods(ImageProcessingEntity): _LOGGER.warning("Detector does not support label %s", label) continue self._label_areas[label] = [0, 0, 1, 1] + self._label_covers[label] = True if label not in dconfig or dconfig[label] > confidence: dconfig[label] = confidence From ac4d8ee07f8164571b5223c90207e84b5a75efbd Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 6 Nov 2019 09:51:20 +0100 Subject: [PATCH 192/306] Upgrade youtube_dl to 2019.11.05 (#28578) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index bb990fc28e7..f413ffd16db 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.10.29" + "youtube_dl==2019.11.05" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 86400df014c..ead7762d60c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2036,7 +2036,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.10.29 +youtube_dl==2019.11.05 # homeassistant.components.zengge zengge==0.2 From 2e1d05560f4fb7d2b4b03dd9208a97e8e6999448 Mon Sep 17 00:00:00 2001 From: temeteke Date: Wed, 6 Nov 2019 21:47:34 +0900 Subject: [PATCH 193/306] Reset states when connection to MPC-HC is lost (#27541) * Reset states when connection to MPC-HC is lost * Add the available property of mpchc --- homeassistant/components/mpchc/media_player.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index ae96704be58..580156a5653 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -69,6 +69,7 @@ class MpcHcDevice(MediaPlayerDevice): self._name = name self._url = url self._player_variables = dict() + self._available = False def update(self): """Get the latest details.""" @@ -79,8 +80,11 @@ class MpcHcDevice(MediaPlayerDevice): for var in mpchc_variables: self._player_variables[var[0]] = var[1].lower() + self._available = True except requests.exceptions.RequestException: _LOGGER.error("Could not connect to MPC-HC at: %s", self._url) + self._player_variables = dict() + self._available = False def _send_command(self, command_id): """Send a command to MPC-HC via its window message ID.""" @@ -111,6 +115,11 @@ class MpcHcDevice(MediaPlayerDevice): return STATE_IDLE + @property + def available(self): + """Return True if entity is available.""" + return self._available + @property def media_title(self): """Return the title of current playing media.""" From b7153ca2073d9ae7d73bf9ca642a850c277cca03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20K=C3=B6ck?= Date: Wed, 6 Nov 2019 13:50:54 +0100 Subject: [PATCH 194/306] Add mqtt temp_low/high_template in SCHEMA_BASE (#28257) * fix missing temp_low/high_template in SCHEMA_BASE * temperature_high/low_state_template test * Update test_climate.py * paste error * Update test_climate.py * Update test_climate.py * Update test_climate.py * Update test_climate.py --- homeassistant/components/mqtt/climate.py | 2 ++ tests/components/mqtt/test_climate.py | 33 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 5c1c9286c6e..4b163c523fa 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -214,7 +214,9 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 064b5743478..3f4fc657186 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -568,6 +568,39 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert state.state == STATE_UNAVAILABLE +async def test_set_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog): + """Test setting of temperature high/low templates.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["climate"]["temperature_low_state_topic"] = "temperature-state" + config["climate"]["temperature_high_state_topic"] = "temperature-state" + config["climate"]["temperature_low_state_template"] = "{{ value_json.temp_low }}" + config["climate"]["temperature_high_state_template"] = "{{ value_json.temp_high }}" + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + + # Temperature - with valid value + assert state.attributes.get("target_temp_low") is None + assert state.attributes.get("target_temp_high") is None + + async_fire_mqtt_message( + hass, "temperature-state", '{"temp_low": "1031", "temp_high": "1032"}' + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 1031 + assert state.attributes.get("target_temp_high") == 1032 + + # Temperature - with invalid value + async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"') + state = hass.states.get(ENTITY_CLIMATE) + # make sure, the invalid value gets logged... + assert "Could not parse temperature from" in caplog.text + # ... but the actual value stays unchanged. + assert state.attributes.get("target_temp_low") == 1031 + assert state.attributes.get("target_temp_high") == 1032 + + async def test_set_with_templates(hass, mqtt_mock, caplog): """Test setting of new fan mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) From 9ba3abd1b73f80b17d7e1be552d91d5b0b67e124 Mon Sep 17 00:00:00 2001 From: ssenart <37013755+ssenart@users.noreply.github.com> Date: Wed, 6 Nov 2019 13:52:59 +0100 Subject: [PATCH 195/306] Add Netatmo camera services (#27970) * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Add Presence Netatmo Camera services (set_light_auto, set_light_on, set_light_off) to control its internal flood light status. * Add Presence Netatmo Camera services (set_light_auto, set_light_on, set_light_off) to control its internal flood light status. * Netatmo camera : Use new style string formatting. * Make the file compliant with flake8 linter. * Make the file compliant with flake8 linter. * Make it compliant with black formatter. * Make it compliant with black formatter. * Bug fix : Flood light control was not working with VPN url. --- homeassistant/components/netatmo/camera.py | 307 +++++++++++++++++- .../components/netatmo/services.yaml | 24 +- 2 files changed, 312 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index ecc38add3b4..f3bf6a6784c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -5,11 +5,20 @@ from pyatmo import NoDevice import requests import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM -from homeassistant.const import CONF_VERIFY_SSL +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, + Camera, + SUPPORT_STREAM, + CAMERA_SERVICE_SCHEMA, +) +from homeassistant.const import CONF_VERIFY_SSL, STATE_ON, STATE_OFF from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, + async_dispatcher_connect, +) -from .const import DATA_NETATMO_AUTH +from .const import DATA_NETATMO_AUTH, DOMAIN from . import CameraData _LOGGER = logging.getLogger(__name__) @@ -33,6 +42,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up access to Netatmo cameras.""" @@ -63,6 +74,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except NoDevice: return None + async def async_service_handler(call): + """Handle service call.""" + _LOGGER.debug( + "Service handler invoked with service=%s and data=%s", + call.service, + call.data, + ) + service = call.service + entity_id = call.data["entity_id"][0] + async_dispatcher_send(hass, f"{service}_{entity_id}") + + hass.services.async_register( + DOMAIN, "set_light_auto", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, "set_light_on", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, "set_light_off", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + class NetatmoCamera(Camera): """Representation of the images published from a Netatmo camera.""" @@ -72,16 +104,39 @@ class NetatmoCamera(Camera): super().__init__() self._data = data self._camera_name = camera_name - self._verify_ssl = verify_ssl - self._quality = quality + self._home = home if home: self._name = home + " / " + camera_name else: self._name = camera_name - self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( - camera=camera_name - ) self._cameratype = camera_type + self._verify_ssl = verify_ssl + self._quality = quality + + # URLs. + self._vpnurl = None + self._localurl = None + + # Identifier + self._id = None + + # Monitoring status. + self._status = None + + # SD Card status + self._sd_status = None + + # Power status + self._alim_status = None + + # Is local + self._is_local = None + + # VPN URL + self._vpn_url = None + + # Light mode status + self._light_mode_status = None def camera_image(self): """Return a still image response from the camera.""" @@ -112,16 +167,79 @@ class NetatmoCamera(Camera): return None return response.content + # Entity property overrides + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return True + @property def name(self): """Return the name of this Netatmo camera device.""" return self._name + @property + def device_state_attributes(self): + """Return the Netatmo-specific camera state attributes.""" + + _LOGGER.debug("Getting new attributes from camera netatmo '%s'", self._name) + + attr = {} + attr["id"] = self._id + attr["status"] = self._status + attr["sd_status"] = self._sd_status + attr["alim_status"] = self._alim_status + attr["is_local"] = self._is_local + attr["vpn_url"] = self._vpn_url + + if self.model == "Presence": + attr["light_mode_status"] = self._light_mode_status + + _LOGGER.debug("Attributes of '%s' = %s", self._name, attr) + + return attr + + @property + def available(self): + """Return True if entity is available.""" + return bool(self._alim_status == "on") + + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_STREAM + + @property + def is_recording(self): + """Return true if the device is recording.""" + return bool(self._status == "on") + @property def brand(self): """Return the camera brand.""" return "Netatmo" + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return bool(self._status == "on") + + @property + def is_on(self): + """Return true if on.""" + return self.is_streaming + + async def stream_source(self): + """Return the stream source.""" + url = "{0}/live/files/{1}/index.m3u8" + if self._localurl: + return url.format(self._localurl, self._quality) + return url.format(self._vpnurl, self._quality) + @property def model(self): """Return the camera model.""" @@ -131,14 +249,167 @@ class NetatmoCamera(Camera): return "Welcome" return None - @property - def supported_features(self): - """Return supported features.""" - return SUPPORT_STREAM + # Other Entity method overrides - async def stream_source(self): - """Return the stream source.""" - url = "{0}/live/files/{1}/index.m3u8" - if self._localurl: - return url.format(self._localurl, self._quality) - return url.format(self._vpnurl, self._quality) + async def async_added_to_hass(self): + """Subscribe to signals and add camera to list.""" + _LOGGER.debug("Registering services for entity_id=%s", self.entity_id) + async_dispatcher_connect( + self.hass, f"set_light_auto_{self.entity_id}", self.set_light_auto + ) + async_dispatcher_connect( + self.hass, f"set_light_on_{self.entity_id}", self.set_light_on + ) + async_dispatcher_connect( + self.hass, f"set_light_off_{self.entity_id}", self.set_light_off + ) + + def update(self): + """Update entity status.""" + + _LOGGER.debug("Updating camera netatmo '%s'", self._name) + + # Refresh camera data. + self._data.update() + + # URLs. + self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + + # Identifier + self._id = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["id"] + + # Monitoring status. + self._status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["status"] + + _LOGGER.debug("Status of '%s' = %s", self._name, self._status) + + # SD Card status + self._sd_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["sd_status"] + + # Power status + self._alim_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["alim_status"] + + # Is local + self._is_local = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["is_local"] + + # VPN URL + self._vpn_url = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["vpn_url"] + + self.is_streaming = self._alim_status == "on" + + if self.model == "Presence": + # Light mode status + self._light_mode_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["light_mode_status"] + + # Camera method overrides + + def enable_motion_detection(self): + """Enable motion detection in the camera.""" + _LOGGER.debug("Enable motion detection of the camera '%s'", self._name) + self._enable_motion_detection(True) + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + _LOGGER.debug("Disable motion detection of the camera '%s'", self._name) + self._enable_motion_detection(False) + + def _enable_motion_detection(self, enable): + """Enable or disable motion detection.""" + try: + if self._localurl: + requests.get( + f"{self._localurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", + timeout=10, + ) + elif self._vpnurl: + requests.get( + f"{self._vpnurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", + timeout=10, + verify=self._verify_ssl, + ) + else: + _LOGGER.error("Welcome/Presence VPN URL is None") + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + except requests.exceptions.RequestException as error: + _LOGGER.error("Welcome/Presence URL changed: %s", error) + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + else: + self.async_schedule_update_ha_state(True) + + # Netatmo Presence specific camera method. + + def set_light_auto(self): + """Set flood light in automatic mode.""" + _LOGGER.debug( + "Set the flood light in automatic mode for the camera '%s'", self._name + ) + self._set_light_mode("auto") + + def set_light_on(self): + """Set flood light on.""" + _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) + self._set_light_mode("on") + + def set_light_off(self): + """Set flood light off.""" + _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) + self._set_light_mode("off") + + def _set_light_mode(self, mode): + """Set light mode ('auto', 'on', 'off').""" + if self.model == "Presence": + try: + config = '{"mode":"' + mode + '"}' + if self._localurl: + requests.get( + f"{self._localurl}/command/floodlight_set_config?config={config}", + timeout=10, + ) + elif self._vpnurl: + requests.get( + f"{self._vpnurl}/command/floodlight_set_config?config={config}", + timeout=10, + verify=self._verify_ssl, + ) + else: + _LOGGER.error("Presence VPN URL is None") + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + except requests.exceptions.RequestException as error: + _LOGGER.error("Presence URL changed: %s", error) + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + else: + self.async_schedule_update_ha_state(True) + else: + _LOGGER.error("Unsupported camera model for light mode") diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 7bb990caf97..a928f4765e0 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -4,5 +4,27 @@ addwebhook: 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 + description: Drop active webhooks. + +set_light_auto: + description: Set the camera (Presence only) light in automatic mode. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' + +set_light_on: + description: Set the camera (Netatmo Presence only) light on. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' + +set_light_off: + description: Set the camera (Netatmo Presence only) light off. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' From 9a0a889492d06855bdb1f6683aa5c46a10aa4926 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Nov 2019 08:32:33 -0800 Subject: [PATCH 196/306] Fix token sent to Almond Web (#28584) --- homeassistant/components/almond/.translations/en.json | 3 ++- homeassistant/components/almond/__init__.py | 6 +++--- homeassistant/components/almond/strings.json | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json index 89134cbb170..3ee811b8326 100644 --- a/homeassistant/components/almond/.translations/en.json +++ b/homeassistant/components/almond/.translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "You can only configure one Almond account.", - "cannot_connect": "Unable to connect to the Almond server." + "cannot_connect": "Unable to connect to the Almond server.", + "missing_configuration": "Please check the documentation on how to set up Almond." }, "title": "Almond" } diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index ebdddecdec3..115e0d24de4 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -111,7 +111,7 @@ async def async_setup_entry(hass, entry): agent = AlmondAgent(api) # Hass.io does its own configuration of Almond. - if entry.data.get("is_hassio"): + if entry.data.get("is_hassio") or entry.data["type"] != TYPE_LOCAL: conversation.async_set_agent(hass, agent) return True @@ -192,10 +192,10 @@ class AlmondOAuth(AbstractAlmondWebAuth): async def async_get_access_token(self): """Return a valid access token.""" - if not self._oauth_session.is_valid: + if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() - return self._oauth_session.token + return self._oauth_session.token["access_token"] class AlmondAgent(conversation.AbstractConversationAgent): diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index 9bc4b0e1b93..5cfc52044bb 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "You can only configure one Almond account.", - "cannot_connect": "Unable to connect to the Almond server." + "cannot_connect": "Unable to connect to the Almond server.", + "missing_configuration": "Please check the documentation on how to set up Almond." }, "title": "Almond" } From d9edd425325d39d1a34cfeb56c3c7f46f4d0da85 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Nov 2019 10:55:56 -0800 Subject: [PATCH 197/306] Update to latest Somfy changes (#28207) * Update to latest Somfy changes * Update api.py * Update api.py --- homeassistant/components/somfy/__init__.py | 10 ++---- homeassistant/components/somfy/api.py | 37 +++++--------------- homeassistant/components/somfy/cover.py | 8 ++--- homeassistant/components/somfy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 16 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index cd5960bf6b1..b767ea83431 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -9,6 +9,7 @@ import logging from datetime import timedelta import voluptuous as vol +from requests import HTTPError from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.components.somfy import config_flow @@ -156,13 +157,8 @@ class SomfyEntity(Entity): @Throttle(SCAN_INTERVAL) async def update_all_devices(hass): """Update all the devices.""" - from requests import HTTPError - from oauthlib.oauth2 import TokenExpiredError - try: data = hass.data[DOMAIN] data[DEVICES] = await hass.async_add_executor_job(data[API].get_devices) - except TokenExpiredError: - _LOGGER.warning("Cannot update devices due to expired token") - except HTTPError: - _LOGGER.warning("Cannot update devices") + except HTTPError as err: + _LOGGER.warning("Cannot update devices: %s", err.response.status_code) diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py index 3e7bcf9deb4..1cfea8ff7d8 100644 --- a/homeassistant/components/somfy/api.py +++ b/homeassistant/components/somfy/api.py @@ -1,15 +1,14 @@ """API for Somfy bound to HASS OAuth.""" from asyncio import run_coroutine_threadsafe -from functools import partial +from typing import Dict, Union -import requests from pymfy.api import somfy_api from homeassistant import core, config_entries from homeassistant.helpers import config_entry_oauth2_flow -class ConfigEntrySomfyApi(somfy_api.AbstractSomfyApi): +class ConfigEntrySomfyApi(somfy_api.SomfyApi): """Provide a Somfy API tied into an OAuth2 based config entry.""" def __init__( @@ -24,32 +23,12 @@ class ConfigEntrySomfyApi(somfy_api.AbstractSomfyApi): self.session = config_entry_oauth2_flow.OAuth2Session( hass, config_entry, implementation ) + super().__init__(None, None, token=self.session.token) - def get(self, path): - """Fetch a URL from the Somfy API.""" - return run_coroutine_threadsafe( - self._request("get", path), self.hass.loop + def refresh_tokens(self,) -> Dict[str, Union[str, int]]: + """Refresh and return new Somfy tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop ).result() - def post(self, path, *, json): - """Post data to the Somfy API.""" - return run_coroutine_threadsafe( - self._request("post", path, json=json), self.hass.loop - ).result() - - async def _request(self, method, path, **kwargs): - """Make a request.""" - await self.session.async_ensure_token_valid() - - return await self.hass.async_add_executor_job( - partial( - requests.request, - method, - f"{self.base_url}{path}", - **kwargs, - headers={ - **kwargs.get("headers", {}), - "authorization": f"Bearer {self.config_entry.data['token']['access_token']}", - }, - ) - ) + return self.session.token diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 5a6d97dee69..d54e7c99001 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -4,6 +4,8 @@ Support for Somfy Covers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.somfy/ """ +from pymfy.api.devices.category import Category +from pymfy.api.devices.blind import Blind from homeassistant.components.cover import ( CoverDevice, @@ -18,8 +20,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def get_covers(): """Retrieve covers.""" - from pymfy.api.devices.category import Category - categories = { Category.ROLLER_SHUTTER.value, Category.INTERIOR_BLIND.value, @@ -51,15 +51,11 @@ class SomfyCover(SomfyEntity, CoverDevice): def __init__(self, device, api): """Initialize the Somfy device.""" - from pymfy.api.devices.blind import Blind - super().__init__(device, api) self.cover = Blind(self.device, self.api) async def async_update(self): """Update the device with the latest data.""" - from pymfy.api.devices.blind import Blind - await super().async_update() self.cover = Blind(self.device, self.api) diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index a34023f76ff..f5a17275bcb 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.6.0"] + "requirements": ["pymfy==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ead7762d60c..3f5ac68cc8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1313,7 +1313,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.somfy -pymfy==0.6.0 +pymfy==0.6.1 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50f4ae5f2a0..ecf8d00d648 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -439,7 +439,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.somfy -pymfy==0.6.0 +pymfy==0.6.1 # homeassistant.components.mochad pymochad==0.2.0 From bb37bc32e3a61fd521b10265292a4848bc83bc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 6 Nov 2019 21:38:00 +0200 Subject: [PATCH 198/306] Always run flake8 through pre-commit, and with doctests (#28490) * Enable flake8 doctests everywhere * Always run flake8 through pre-commit --- .vscode/tasks.json | 2 +- script/lazytox.py | 2 +- script/lint | 2 +- setup.cfg | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 151868a1663..1a0bfb16a9b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -33,7 +33,7 @@ { "label": "Flake8", "type": "shell", - "command": "flake8 homeassistant tests", + "command": "pre-commit run flake8 --all-files", "group": { "kind": "test", "isDefault": true diff --git a/script/lazytox.py b/script/lazytox.py index 026d4639a06..ca8a160e7dc 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -120,7 +120,7 @@ async def pylint(files): async def flake8(files): """Exec flake8.""" - _, log = await async_exec("flake8", "--doctests", *files) + _, log = await async_exec("pre-commit", "run", "flake8", "--files", *files) res = [] for line in log.splitlines(): line = line.split(":") diff --git a/script/lint b/script/lint index 8ba14d8939e..e4bf74cf602 100755 --- a/script/lint +++ b/script/lint @@ -15,7 +15,7 @@ printf "%s\n" $files echo "================" echo "LINT with flake8" echo "================" -flake8 --doctests $files +pre-commit run flake8 --files $files echo "================" echo "LINT with pylint" echo "================" diff --git a/setup.cfg b/setup.cfg index 6d0e5378b44..bb2b1652ffa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ norecursedirs = .git testing_config [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +doctests = True # To work with Black max-line-length = 88 # E501: line too long From 3d2ff841d338421ce79d2f38aa41eaaeb7a303cb Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Wed, 6 Nov 2019 22:46:18 +0100 Subject: [PATCH 199/306] Handle exceptions from PyViCare library (#28536) * ViCare: Handle exceptions from PyViCare library (#28072) Sometimes Viessmann server failures or other connection problems may lead to exceptions thrown when updating data. This commit handles those exceptions with some error logging and makes sure that the component does not break completely in that case. * Remove unneeded returns * Remove unneeded returns --- homeassistant/components/vicare/climate.py | 93 +++++++++++-------- .../components/vicare/water_heater.py | 25 +++-- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index fe162c0c837..7e330383b30 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,5 +1,7 @@ """Viessmann ViCare climate device.""" import logging +import requests +import simplejson from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -111,55 +113,64 @@ class ViCareClimate(ClimateDevice): def update(self): """Let HA know there has been an update from the ViCare API.""" - _room_temperature = self._api.getRoomTemperature() - _supply_temperature = self._api.getSupplyTemperature() - if _room_temperature is not None and _room_temperature != PYVICARE_ERROR: - self._current_temperature = _room_temperature - elif _supply_temperature != PYVICARE_ERROR: - self._current_temperature = _supply_temperature - else: - self._current_temperature = None - self._current_program = self._api.getActiveProgram() + try: + _room_temperature = self._api.getRoomTemperature() + _supply_temperature = self._api.getSupplyTemperature() + if _room_temperature is not None and _room_temperature != PYVICARE_ERROR: + self._current_temperature = _room_temperature + elif _supply_temperature != PYVICARE_ERROR: + self._current_temperature = _supply_temperature + else: + self._current_temperature = None + self._current_program = self._api.getActiveProgram() - # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby - desired_temperature = self._api.getCurrentDesiredTemperature() - if desired_temperature == PYVICARE_ERROR: - desired_temperature = None + # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby + desired_temperature = self._api.getCurrentDesiredTemperature() + if desired_temperature == PYVICARE_ERROR: + desired_temperature = None - self._target_temperature = desired_temperature + self._target_temperature = desired_temperature - self._current_mode = self._api.getActiveMode() + self._current_mode = self._api.getActiveMode() - # Update the generic device attributes - self._attributes = {} - self._attributes["room_temperature"] = _room_temperature - self._attributes["supply_temperature"] = _supply_temperature - self._attributes["outside_temperature"] = self._api.getOutsideTemperature() - self._attributes["active_vicare_program"] = self._current_program - self._attributes["active_vicare_mode"] = self._current_mode - self._attributes["heating_curve_slope"] = self._api.getHeatingCurveSlope() - self._attributes["heating_curve_shift"] = self._api.getHeatingCurveShift() - self._attributes[ - "month_since_last_service" - ] = self._api.getMonthSinceLastService() - self._attributes["date_last_service"] = self._api.getLastServiceDate() - self._attributes["error_history"] = self._api.getErrorHistory() - self._attributes["active_error"] = self._api.getActiveError() - self._attributes[ - "circulationpump_active" - ] = self._api.getCirculationPumpActive() + # Update the generic device attributes + self._attributes = {} + self._attributes["room_temperature"] = _room_temperature + self._attributes["supply_temperature"] = _supply_temperature + self._attributes["outside_temperature"] = self._api.getOutsideTemperature() + self._attributes["active_vicare_program"] = self._current_program + self._attributes["active_vicare_mode"] = self._current_mode + self._attributes["heating_curve_slope"] = self._api.getHeatingCurveSlope() + self._attributes["heating_curve_shift"] = self._api.getHeatingCurveShift() + self._attributes[ + "month_since_last_service" + ] = self._api.getMonthSinceLastService() + self._attributes["date_last_service"] = self._api.getLastServiceDate() + self._attributes["error_history"] = self._api.getErrorHistory() + self._attributes["active_error"] = self._api.getActiveError() + self._attributes[ + "circulationpump_active" + ] = self._api.getCirculationPumpActive() - # Update the specific device attributes - if self._heating_type == HeatingType.gas: - self._current_action = self._api.getBurnerActive() + # Update the specific device attributes + if self._heating_type == HeatingType.gas: + self._current_action = self._api.getBurnerActive() - self._attributes["burner_modulation"] = self._api.getBurnerModulation() - self._attributes["boiler_temperature"] = self._api.getBoilerTemperature() + self._attributes["burner_modulation"] = self._api.getBurnerModulation() + self._attributes[ + "boiler_temperature" + ] = self._api.getBoilerTemperature() - elif self._heating_type == HeatingType.heatpump: - self._current_action = self._api.getCompressorActive() + elif self._heating_type == HeatingType.heatpump: + self._current_action = self._api.getCompressorActive() - self._attributes["return_temperature"] = self._api.getReturnTemperature() + self._attributes[ + "return_temperature" + ] = self._api.getReturnTemperature() + except requests.exceptions.ConnectionError: + _LOGGER.error("Unable to retrieve data from ViCare server") + except simplejson.errors.JSONDecodeError: + _LOGGER.error("Unable to decode data from ViCare server") @property def supported_features(self): diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 7c4968ad0a4..1f56c46dc1c 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,5 +1,7 @@ """Viessmann ViCare water_heater device.""" import logging +import requests +import simplejson from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, @@ -41,6 +43,8 @@ HA_TO_VICARE_HVAC_DHW = { OPERATION_MODE_ON: VICARE_MODE_DHW, } +PYVICARE_ERROR = "error" + def setup_platform(hass, config, add_entities, discovery_info=None): """Create the ViCare water_heater devices.""" @@ -75,15 +79,22 @@ class ViCareWater(WaterHeaterDevice): def update(self): """Let HA know there has been an update from the ViCare API.""" - current_temperature = self._api.getDomesticHotWaterStorageTemperature() - if current_temperature is not None and current_temperature != "error": - self._current_temperature = current_temperature - else: - self._current_temperature = None + try: + current_temperature = self._api.getDomesticHotWaterStorageTemperature() + if current_temperature != PYVICARE_ERROR: + self._current_temperature = current_temperature + else: + self._current_temperature = None - self._target_temperature = self._api.getDomesticHotWaterConfiguredTemperature() + self._target_temperature = ( + self._api.getDomesticHotWaterConfiguredTemperature() + ) - self._current_mode = self._api.getActiveMode() + self._current_mode = self._api.getActiveMode() + except requests.exceptions.ConnectionError: + _LOGGER.error("Unable to retrieve data from ViCare server") + except simplejson.errors.JSONDecodeError: + _LOGGER.error("Unable to decode data from ViCare server") @property def supported_features(self): From 78b83c653a6ffe8ce6cd62ccea067f3c146163e3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Nov 2019 23:55:39 +0100 Subject: [PATCH 200/306] Add WLED integration (#28542) * Add WLED integration * Use f-string for uniq id in sensor platform * Typing improvements * Removes sensor & light platform * Remove PARALLEL_UPDATES from integration level * Correct type in code comment 'themselves' * Use async_track_time_interval in async context * Remove stale code * Remove decorator from Flow handler * Remove unused __init__ from config flow * Move show form methods to sync * Only wrap lines that can raise in try except block * Remove domain and platform from uniq id * Wrap light state in bool object in is_on method * Use async_schedule_update_ha_state in async context * Return empty dict in device state attributes instead of None * Remove unneeded setdefault call in setup entry * Cancel update timer on entry unload * Restructure config flow code * Adjust tests for new uniq id * Correct typo AdGuard Home -> WLED in config flow file comment * Convert internal package imports to be relative * Reformat JSON files with Prettier * Improve tests based on review comments * Add test for zeroconf when no data is provided * Cleanup and extended tests --- CODEOWNERS | 1 + .../components/wled/.translations/en.json | 26 +++ homeassistant/components/wled/__init__.py | 182 +++++++++++++++ homeassistant/components/wled/config_flow.py | 123 ++++++++++ homeassistant/components/wled/const.py | 25 ++ homeassistant/components/wled/light.py | 219 ++++++++++++++++++ homeassistant/components/wled/manifest.json | 10 + homeassistant/components/wled/strings.json | 26 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 3 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/wled/__init__.py | 42 ++++ tests/components/wled/test_config_flow.py | 207 +++++++++++++++++ tests/components/wled/test_init.py | 60 +++++ tests/components/wled/test_light.py | 179 ++++++++++++++ tests/fixtures/wled/rgb.json | 210 +++++++++++++++++ tests/fixtures/wled/rgbw.json | 198 ++++++++++++++++ 18 files changed, 1518 insertions(+) create mode 100644 homeassistant/components/wled/.translations/en.json create mode 100644 homeassistant/components/wled/__init__.py create mode 100644 homeassistant/components/wled/config_flow.py create mode 100644 homeassistant/components/wled/const.py create mode 100644 homeassistant/components/wled/light.py create mode 100644 homeassistant/components/wled/manifest.json create mode 100644 homeassistant/components/wled/strings.json create mode 100644 tests/components/wled/__init__.py create mode 100644 tests/components/wled/test_config_flow.py create mode 100644 tests/components/wled/test_init.py create mode 100644 tests/components/wled/test_light.py create mode 100644 tests/fixtures/wled/rgb.json create mode 100644 tests/fixtures/wled/rgbw.json diff --git a/CODEOWNERS b/CODEOWNERS index ceb58f370d4..27e06d874e1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -339,6 +339,7 @@ homeassistant/components/weblink/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra +homeassistant/components/wled/* @frenck homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya homeassistant/components/xbox_live/* @MartinHjelmare diff --git a/homeassistant/components/wled/.translations/en.json b/homeassistant/components/wled/.translations/en.json new file mode 100644 index 00000000000..dde66b8e122 --- /dev/null +++ b/homeassistant/components/wled/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "WLED", + "flow_title": "WLED: {name}", + "step": { + "user": { + "title": "Link your WLED", + "description": "Set up your WLED to integrate with Home Assistant.", + "data": { + "host": "Host or IP address" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the WLED named `{name}` to Home Assistant?", + "title": "Discovered WLED device" + } + }, + "error": { + "connection_error": "Failed to connect to WLED device." + }, + "abort": { + "already_configured": "This WLED device is already configured.", + "connection_error": "Failed to connect to WLED device." + } + } +} diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py new file mode 100644 index 00000000000..62f611b18ec --- /dev/null +++ b/homeassistant/components/wled/__init__.py @@ -0,0 +1,182 @@ +"""Support for WLED.""" +from datetime import timedelta +import logging +from typing import Any, Dict, Optional, Union + +from wled import WLED, WLEDConnectionError, WLEDError + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +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 +from homeassistant.util import dt as dt_util + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, + DATA_WLED_CLIENT, + DATA_WLED_TIMER, + DATA_WLED_UPDATED, + DOMAIN, +) + +SCAN_INTERVAL = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the WLED components.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up WLED from a config entry.""" + + # Create WLED instance for this entry + session = async_get_clientsession(hass) + wled = WLED(entry.data[CONF_HOST], loop=hass.loop, session=session) + + # Ensure we can connect and talk to it + try: + await wled.update() + except WLEDConnectionError as exception: + raise ConfigEntryNotReady from exception + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} + + # Set up all platforms for this device/entry. + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) + ) + + async def interval_update(now: dt_util.dt.datetime = None) -> None: + """Poll WLED device function, dispatches event after update.""" + try: + await wled.update() + except WLEDError: + _LOGGER.debug("An error occurred while updating WLED", exc_info=True) + + # Even if the update failed, we still send out the event. + # To allow entities to make themselves unavailable. + async_dispatcher_send(hass, DATA_WLED_UPDATED, entry.entry_id) + + # Schedule update interval + hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER] = async_track_time_interval( + hass, interval_update, SCAN_INTERVAL + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload WLED config entry.""" + + # Cancel update timer for this entry/device. + cancel_timer = hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER] + cancel_timer() + + # Unload entities for this entry/device. + await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) + + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + + return True + + +class WLEDEntity(Entity): + """Defines a base WLED entity.""" + + def __init__(self, entry_id: str, wled: WLED, name: str, icon: str) -> None: + """Initialize the WLED entity.""" + self._attributes: Dict[str, Union[str, int, float]] = {} + self._available = True + self._entry_id = entry_id + self._icon = icon + self._name = name + self._unsub_dispatcher = None + self.wled = wled + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def should_poll(self) -> bool: + """Return the polling requirement of the entity.""" + return False + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + return self._attributes + + async def async_added_to_hass(self) -> None: + """Connect to dispatcher listening for entity data notifications.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, DATA_WLED_UPDATED, self._schedule_immediate_update + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect from update signal.""" + self._unsub_dispatcher() + + @callback + def _schedule_immediate_update(self, entry_id: str) -> None: + """Schedule an immediate update of the entity.""" + if entry_id == self._entry_id: + self.async_schedule_update_ha_state(True) + + async def async_update(self) -> None: + """Update WLED entity.""" + if self.wled.device is None: + self._available = False + return + + self._available = True + await self._wled_update() + + async def _wled_update(self) -> None: + """Update WLED entity.""" + raise NotImplementedError() + + +class WLEDDeviceEntity(WLEDEntity): + """Defines a WLED device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this WLED device.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self.wled.device.info.mac_address)}, + ATTR_NAME: self.wled.device.info.name, + ATTR_MANUFACTURER: self.wled.device.info.brand, + ATTR_MODEL: self.wled.device.info.product, + ATTR_SOFTWARE_VERSION: self.wled.device.info.version, + } diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py new file mode 100644 index 00000000000..7be283874e0 --- /dev/null +++ b/homeassistant/components/wled/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow to configure the WLED integration.""" +import logging +from typing import Any, Dict, Optional + +import voluptuous as vol +from wled import WLED, WLEDConnectionError + +from homeassistant.config_entries import ( + CONN_CLASS_LOCAL_POLL, + SOURCE_ZEROCONF, + ConfigFlow, +) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.helpers import ConfigType +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint: disable=W0611 + +_LOGGER = logging.getLogger(__name__) + + +class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a WLED config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + return await self._handle_config_flow(user_input) + + async def async_step_zeroconf( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle zeroconf discovery.""" + if user_input is None: + return self.async_abort(reason="connection_error") + + # Hostname is format: wled-livingroom.local. + host = user_input["hostname"].rstrip(".") + name, _ = host.rsplit(".") + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update( + {CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": name}} + ) + + # Prepare configuration flow + return await self._handle_config_flow(user_input, True) + + async def async_step_zeroconf_confirm( + self, user_input: ConfigType = None + ) -> Dict[str, Any]: + """Handle a flow initiated by zeroconf.""" + return await self._handle_config_flow(user_input) + + async def _handle_config_flow( + self, user_input: Optional[ConfigType] = None, prepare: bool = False + ) -> Dict[str, Any]: + """Config flow handler for WLED.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + source = self.context.get("source") + + # Request user input, unless we are preparing discovery flow + if user_input is None and not prepare: + if source == SOURCE_ZEROCONF: + return self._show_confirm_dialog() + return self._show_setup_form() + + if source == SOURCE_ZEROCONF: + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + user_input[CONF_HOST] = self.context.get(CONF_HOST) + + errors = {} + session = async_get_clientsession(self.hass) + wled = WLED(user_input[CONF_HOST], loop=self.hass.loop, session=session) + + try: + device = await wled.update() + except WLEDConnectionError: + if source == SOURCE_ZEROCONF: + return self.async_abort(reason="connection_error") + errors["base"] = "connection_error" + return self._show_setup_form(errors) + + # Check if already configured + mac_address = device.info.mac_address + for entry in self._async_current_entries(): + if entry.data[CONF_MAC] == mac_address: + # This mac address is already configured + return self.async_abort(reason="already_configured") + + title = user_input[CONF_HOST] + if source == SOURCE_ZEROCONF: + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + title = self.context.get(CONF_NAME) + + if prepare: + return await self.async_step_zeroconf_confirm() + + return self.async_create_entry( + title=title, data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: mac_address} + ) + + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors or {}, + ) + + def _show_confirm_dialog(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the confirm dialog to the user.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + name = self.context.get(CONF_NAME) + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"name": name}, + errors=errors or {}, + ) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py new file mode 100644 index 00000000000..9bc5f64a444 --- /dev/null +++ b/homeassistant/components/wled/const.py @@ -0,0 +1,25 @@ +"""Constants for the WLED integration.""" + +# Integration domain +DOMAIN = "wled" + +# Hass data keys +DATA_WLED_CLIENT = "wled_client" +DATA_WLED_TIMER = "wled_timer" +DATA_WLED_UPDATED = "wled_updated" + +# Attributes +ATTR_COLOR_PRIMARY = "color_primary" +ATTR_DURATION = "duration" +ATTR_IDENTIFIERS = "identifiers" +ATTR_INTENSITY = "intensity" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_ON = "on" +ATTR_PALETTE = "palette" +ATTR_PLAYLIST = "playlist" +ATTR_PRESET = "preset" +ATTR_SEGMENT_ID = "segment_id" +ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_SPEED = "speed" +ATTR_TARGET_BRIGHTNESS = "target_brightness" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py new file mode 100644 index 00000000000..3d2c9d6ef2c --- /dev/null +++ b/homeassistant/components/wled/light.py @@ -0,0 +1,219 @@ +"""Support for LED lights.""" +import logging +from typing import Any, Callable, List, Optional, Tuple + +from wled import WLED, Effect, WLEDError + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_TRANSITION, + Light, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.color as color_util + +from . import WLEDDeviceEntity +from .const import ( + ATTR_COLOR_PRIMARY, + ATTR_INTENSITY, + ATTR_ON, + ATTR_PALETTE, + ATTR_PLAYLIST, + ATTR_PRESET, + ATTR_SEGMENT_ID, + ATTR_SPEED, + DATA_WLED_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up WLED light based on a config entry.""" + wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + + # Does the WLED device support RGBW + rgbw = wled.device.info.leds.rgbw + + # List of supported effects + effects = wled.device.effects + + # WLED supports splitting a strip in multiple segments + # Each segment will be a separate light in Home Assistant + lights = [] + for light in wled.device.state.segments: + lights.append(WLEDLight(entry.entry_id, wled, light.segment_id, rgbw, effects)) + + async_add_entities(lights, True) + + +class WLEDLight(Light, WLEDDeviceEntity): + """Defines a WLED light.""" + + def __init__( + self, entry_id: str, wled: WLED, segment: int, rgbw: bool, effects: List[Effect] + ): + """Initialize WLED light.""" + self._effects = effects + self._rgbw = rgbw + self._segment = segment + + self._brightness: Optional[int] = None + self._color: Optional[Tuple[float, float]] = None + self._effect: Optional[str] = None + self._state: Optional[bool] = None + + # Only apply the segment ID if it is not the first segment + name = wled.device.info.name + if segment != 0: + name += f" {segment}" + + super().__init__(entry_id, wled, name, "mdi:led-strip-variant") + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.wled.device.info.mac_address}_{self._segment}" + + @property + def hs_color(self) -> Optional[Tuple[float, float]]: + """Return the hue and saturation color value [float, float].""" + return self._color + + @property + def effect(self) -> Optional[str]: + """Return the current effect of the light.""" + return self._effect + + @property + def brightness(self) -> Optional[int]: + """Return the brightness of this light between 1..255.""" + return self._brightness + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return ( + SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_COLOR_TEMP + | SUPPORT_EFFECT + | SUPPORT_TRANSITION + ) + + @property + def effect_list(self) -> List[str]: + """Return the list of supported effects.""" + return [effect.name for effect in self._effects] + + @property + def is_on(self) -> bool: + """Return the state of the light.""" + return bool(self._state) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + try: + await self.wled.light(on=False) + self._state = False + except WLEDError: + _LOGGER.error("An error occurred while turning off WLED light.") + self._available = False + self.async_schedule_update_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + data = {ATTR_ON: True, ATTR_SEGMENT_ID: self._segment} + + if ATTR_COLOR_TEMP in kwargs: + mireds = color_util.color_temperature_kelvin_to_mired( + kwargs[ATTR_COLOR_TEMP] + ) + data[ATTR_COLOR_PRIMARY] = tuple( + map(int, color_util.color_temperature_to_rgb(mireds)) + ) + + if ATTR_HS_COLOR in kwargs: + hue, sat = kwargs[ATTR_HS_COLOR] + data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) + + if ATTR_TRANSITION in kwargs: + data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION] + + if ATTR_BRIGHTNESS in kwargs: + data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] + + if ATTR_EFFECT in kwargs: + data[ATTR_EFFECT] = kwargs[ATTR_EFFECT] + + # Support for RGBW strips + if self._rgbw and any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): + data[ATTR_COLOR_PRIMARY] = color_util.color_rgb_to_rgbw( + *data[ATTR_COLOR_PRIMARY] + ) + + try: + await self.wled.light(**data) + + self._state = True + + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + if ATTR_EFFECT in kwargs: + self._effect = kwargs[ATTR_EFFECT] + + if ATTR_HS_COLOR in kwargs: + self._color = kwargs[ATTR_HS_COLOR] + + if ATTR_COLOR_TEMP in kwargs: + self._color = color_util.color_temperature_to_hs(mireds) + + except WLEDError: + _LOGGER.error("An error occurred while turning on WLED light.") + self._available = False + self.async_schedule_update_ha_state() + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._brightness = self.wled.device.state.brightness + self._effect = self.wled.device.state.segments[self._segment].effect.name + self._state = self.wled.device.state.on + + color = self.wled.device.state.segments[self._segment].color_primary + if self._rgbw: + color = color_util.color_rgbw_to_rgb(*color) + self._color = color_util.color_RGB_to_hs(*color) + + playlist = self.wled.device.state.playlist + if playlist == -1: + playlist = None + + preset = self.wled.device.state.preset + if preset == -1: + preset = None + + self._attributes = { + ATTR_INTENSITY: self.wled.device.state.segments[self._segment].intensity, + ATTR_PALETTE: self.wled.device.state.segments[self._segment].palette.name, + ATTR_PLAYLIST: playlist, + ATTR_PRESET: preset, + ATTR_SPEED: self.wled.device.state.segments[self._segment].speed, + } diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json new file mode 100644 index 00000000000..97e46998511 --- /dev/null +++ b/homeassistant/components/wled/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "wled", + "name": "WLED", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/wled", + "requirements": ["wled==0.1.0"], + "dependencies": [], + "zeroconf": ["_wled._tcp.local."], + "codeowners": ["@frenck"] +} diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json new file mode 100644 index 00000000000..dde66b8e122 --- /dev/null +++ b/homeassistant/components/wled/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "WLED", + "flow_title": "WLED: {name}", + "step": { + "user": { + "title": "Link your WLED", + "description": "Set up your WLED to integrate with Home Assistant.", + "data": { + "host": "Host or IP address" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the WLED named `{name}` to Home Assistant?", + "title": "Discovered WLED device" + } + }, + "error": { + "connection_error": "Failed to connect to WLED device." + }, + "abort": { + "already_configured": "This WLED device is already configured.", + "connection_error": "Failed to connect to WLED device." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 22d36fc46c6..519df86f5e9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -81,6 +81,7 @@ FLOWS = [ "vesync", "wemo", "withings", + "wled", "wwlln", "zha", "zone", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 6200e2facb0..108fe38e647 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -20,6 +20,9 @@ ZEROCONF = { ], "_hap._tcp.local.": [ "homekit_controller" + ], + "_wled._tcp.local.": [ + "wled" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 3f5ac68cc8e..ac6529ff869 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1995,6 +1995,9 @@ wirelesstagpy==0.4.0 # homeassistant.components.withings withings-api==2.1.3 +# homeassistant.components.wled +wled==0.1.0 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecf8d00d648..605e244ace1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -617,6 +617,9 @@ websockets==6.0 # homeassistant.components.withings withings-api==2.1.3 +# homeassistant.components.wled +wled==0.1.0 + # homeassistant.components.bluesound # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py new file mode 100644 index 00000000000..41cbbf01074 --- /dev/null +++ b/tests/components/wled/__init__.py @@ -0,0 +1,42 @@ +"""Tests for the WLED integration.""" + +from homeassistant.components.wled.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + rgbw: bool = False, + skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the WLED integration in Home Assistant.""" + + fixture = "wled/rgb.json" if not rgbw else "wled/rgbw.json" + aioclient_mock.get( + "http://example.local:80/json/", + text=load_fixture(fixture), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.post( + "http://example.local:80/json/state", + json={"success": True}, + headers={"Content-Type": "application/json"}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "example.local", CONF_MAC: "aabbccddeeff"} + ) + + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py new file mode 100644 index 00000000000..322a068150b --- /dev/null +++ b/tests/components/wled/test_config_flow.py @@ -0,0 +1,207 @@ +"""Tests for the WLED config flow.""" +import aiohttp + +from homeassistant import data_entry_flow +from homeassistant.components.wled import config_flow +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the user set up form is served.""" + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input=None) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: + """Test that the zeroconf confirmation form is served.""" + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF, CONF_NAME: "test"} + result = await flow.async_step_zeroconf_confirm() + + assert result["description_placeholders"] == {CONF_NAME: "test"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zerconf_form( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that the zeroconf confirmation form is served.""" + aioclient_mock.get( + "http://example.local:80/json/", + text=load_fixture("wled/rgb.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf({"hostname": "example.local."}) + + assert flow.context[CONF_HOST] == "example.local" + assert flow.context[CONF_NAME] == "example" + assert result["description_placeholders"] == {CONF_NAME: "example"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on WLED connection error.""" + aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input={CONF_HOST: "example.com"}) + + assert result["errors"] == {"base": "connection_error"} + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_zeroconf_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on WLED connection error.""" + aioclient_mock.get("http://example.local/json/", exc=aiohttp.ClientError) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf(user_input={"hostname": "example.local."}) + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_confirm_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on WLED connection error.""" + aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = { + "source": SOURCE_ZEROCONF, + CONF_HOST: "example.com", + CONF_NAME: "test", + } + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "example.com"} + ) + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_no_data( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort if zeroconf provides no data.""" + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + result = await flow.async_step_zeroconf() + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user({CONF_HOST: "example.local"}) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf({"hostname": "example.local."}) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://example.local:80/json/", + text=load_fixture("wled/rgb.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input=None) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_user(user_input={CONF_HOST: "example.local"}) + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_MAC] == "aabbccddeeff" + assert result["title"] == "example.local" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_full_zeroconf_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://example.local:80/json/", + text=load_fixture("wled/rgb.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf({"hostname": "example.local."}) + + assert flow.context[CONF_HOST] == "example.local" + assert flow.context[CONF_NAME] == "example" + assert result["description_placeholders"] == {CONF_NAME: "example"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "example.local"} + ) + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_MAC] == "aabbccddeeff" + assert result["title"] == "example" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py new file mode 100644 index 00000000000..a565dcfb181 --- /dev/null +++ b/tests/components/wled/test_init.py @@ -0,0 +1,60 @@ +"""Tests for the WLED integration.""" +import aiohttp +from asynctest import patch + +from homeassistant.components.wled.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the WLED configuration entry not ready.""" + aioclient_mock.get("http://example.local:80/json/", exc=aiohttp.ClientError) + + entry = await init_integration(hass, aioclient_mock) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the WLED configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock) + assert hass.data[DOMAIN] + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) + + +async def test_interval_update( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the WLED configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock, skip_setup=True) + + interval_action = False + + def async_track_time_interval(hass, action, interval): + nonlocal interval_action + interval_action = action + + with patch( + "homeassistant.components.wled.async_track_time_interval", + new=async_track_time_interval, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert interval_action + await interval_action() # pylint: disable=not-callable + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py new file mode 100644 index 00000000000..185a25b0507 --- /dev/null +++ b/tests/components/wled/test_light.py @@ -0,0 +1,179 @@ +"""Tests for the WLED light platform.""" +import aiohttp + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.components.wled.const import ( + ATTR_INTENSITY, + ATTR_PALETTE, + ATTR_PLAYLIST, + ATTR_PRESET, + ATTR_SPEED, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_rgb_light_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the WLED lights.""" + await init_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # First segment of the strip + state = hass.states.get("light.wled_rgb_light") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 127 + assert state.attributes.get(ATTR_EFFECT) == "Solid" + assert state.attributes.get(ATTR_HS_COLOR) == (37.412, 100.0) + assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant" + assert state.attributes.get(ATTR_INTENSITY) == 128 + assert state.attributes.get(ATTR_PALETTE) == "Default" + assert state.attributes.get(ATTR_PLAYLIST) is None + assert state.attributes.get(ATTR_PRESET) is None + assert state.attributes.get(ATTR_SPEED) == 32 + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.wled_rgb_light") + assert entry + assert entry.unique_id == "aabbccddeeff_0" + + # Second segment of the strip + state = hass.states.get("light.wled_rgb_light_1") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 127 + assert state.attributes.get(ATTR_EFFECT) == "Blink" + assert state.attributes.get(ATTR_HS_COLOR) == (148.941, 100.0) + assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant" + assert state.attributes.get(ATTR_INTENSITY) == 64 + assert state.attributes.get(ATTR_PALETTE) == "Random Cycle" + assert state.attributes.get(ATTR_PLAYLIST) is None + assert state.attributes.get(ATTR_PRESET) is None + assert state.attributes.get(ATTR_SPEED) == 16 + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.wled_rgb_light_1") + assert entry + assert entry.unique_id == "aabbccddeeff_1" + + +async def test_switch_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the change of state of the WLED switches.""" + await init_integration(hass, aioclient_mock) + + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_EFFECT: "Chase", + ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_RGB_COLOR: [255, 0, 0], + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BRIGHTNESS) == 42 + assert state.attributes.get(ATTR_EFFECT) == "Chase" + assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + + +async def test_light_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the WLED switches.""" + aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError) + await init_integration(hass, aioclient_mock) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_UNAVAILABLE + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgb_light_1"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light_1") + assert state.state == STATE_UNAVAILABLE + + +async def test_rgbw_light( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test RGBW support for WLED.""" + await init_integration(hass, aioclient_mock, rgbw=True) + + state = hass.states.get("light.wled_rgbw_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 64.706) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgbw_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) diff --git a/tests/fixtures/wled/rgb.json b/tests/fixtures/wled/rgb.json new file mode 100644 index 00000000000..70a54f06644 --- /dev/null +++ b/tests/fixtures/wled/rgb.json @@ -0,0 +1,210 @@ +{ + "state": { + "on": true, + "bri": 127, + "transition": 7, + "ps": -1, + "pl": -1, + "nl": { + "on": false, + "dur": 60, + "fade": true, + "tbri": 0 + }, + "udpn": { + "send": false, + "recv": true + }, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 19, + "len": 20, + "col": [[255, 159, 0], [0, 0, 0], [0, 0, 0]], + "fx": 0, + "sx": 32, + "ix": 128, + "pal": 0, + "sel": true, + "rev": false, + "cln": -1 + }, + { + "id": 1, + "start": 20, + "stop": 30, + "len": 10, + "col": [[0, 255, 123], [0, 0, 0], [0, 0, 0]], + "fx": 1, + "sx": 16, + "ix": 64, + "pal": 1, + "sel": true, + "rev": false, + "cln": -1 + } + ] + }, + "info": { + "ver": "0.8.5", + "vid": 1909122, + "leds": { + "count": 30, + "rgbw": false, + "pin": [2], + "pwr": 470, + "maxpwr": 850, + "maxseg": 10 + }, + "name": "WLED RGB Light", + "udpport": 21324, + "live": false, + "fxcount": 81, + "palcount": 50, + "arch": "esp8266", + "core": "2_4_2", + "freeheap": 14600, + "uptime": 32, + "opt": 119, + "brand": "WLED", + "product": "DIY light", + "btype": "bin", + "mac": "aabbccddeeff" + }, + "effects": [ + "Solid", + "Blink", + "Breathe", + "Wipe", + "Wipe Random", + "Random Colors", + "Sweep", + "Dynamic", + "Colorloop", + "Rainbow", + "Scan", + "Dual Scan", + "Fade", + "Chase", + "Chase Rainbow", + "Running", + "Saw", + "Twinkle", + "Dissolve", + "Dissolve Rnd", + "Sparkle", + "Dark Sparkle", + "Sparkle+", + "Strobe", + "Strobe Rainbow", + "Mega Strobe", + "Blink Rainbow", + "Android", + "Chase", + "Chase Random", + "Chase Rainbow", + "Chase Flash", + "Chase Flash Rnd", + "Rainbow Runner", + "Colorful", + "Traffic Light", + "Sweep Random", + "Running 2", + "Red & Blue", + "Stream", + "Scanner", + "Lighthouse", + "Fireworks", + "Rain", + "Merry Christmas", + "Fire Flicker", + "Gradient", + "Loading", + "In Out", + "In In", + "Out Out", + "Out In", + "Circus", + "Halloween", + "Tri Chase", + "Tri Wipe", + "Tri Fade", + "Lightning", + "ICU", + "Multi Comet", + "Dual Scanner", + "Stream 2", + "Oscillate", + "Pride 2015", + "Juggle", + "Palette", + "Fire 2012", + "Colorwaves", + "BPM", + "Fill Noise", + "Noise 1", + "Noise 2", + "Noise 3", + "Noise 4", + "Colortwinkle", + "Lake", + "Meteor", + "Smooth Meteor", + "Railway", + "Ripple", + "Twinklefox" + ], + "palettes": [ + "Default", + "Random Cycle", + "Primary Color", + "Based on Primary", + "Set Colors", + "Based on Set", + "Party", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Rainbow", + "Rainbow Bands", + "Sunset", + "Rivendell", + "Breeze", + "Red & Blue", + "Yellowout", + "Analogous", + "Splash", + "Pastel", + "Sunset 2", + "Beech", + "Vintage", + "Departure", + "Landscape", + "Beach", + "Sherbet", + "Hult", + "Hult 64", + "Drywet", + "Jul", + "Grintage", + "Rewhi", + "Tertiary", + "Fire", + "Icefire", + "Cyane", + "Light Pink", + "Autumn", + "Magenta", + "Magred", + "Yelmag", + "Yelblu", + "Orange & Teal", + "Tiamat", + "April Night", + "Orangery", + "C9", + "Sakura" + ] +} diff --git a/tests/fixtures/wled/rgbw.json b/tests/fixtures/wled/rgbw.json new file mode 100644 index 00000000000..0d51dfedd2d --- /dev/null +++ b/tests/fixtures/wled/rgbw.json @@ -0,0 +1,198 @@ +{ + "state": { + "on": true, + "bri": 140, + "transition": 7, + "ps": -1, + "pl": -1, + "nl": { + "on": false, + "dur": 60, + "fade": true, + "tbri": 0 + }, + "udpn": { + "send": false, + "recv": true + }, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 13, + "len": 13, + "col": [[255, 0, 0, 139], [0, 0, 0, 0], [0, 0, 0, 0]], + "fx": 9, + "sx": 165, + "ix": 128, + "pal": 0, + "sel": true, + "rev": false, + "cln": -1 + } + ] + }, + "info": { + "ver": "0.8.6", + "vid": 1910255, + "leds": { + "count": 13, + "rgbw": true, + "pin": [2], + "pwr": 208, + "maxpwr": 850, + "maxseg": 10 + }, + "name": "WLED RGBW Light", + "udpport": 21324, + "live": false, + "fxcount": 83, + "palcount": 50, + "arch": "esp8266", + "core": "2_5_2", + "freeheap": 20136, + "uptime": 5591, + "opt": 119, + "brand": "WLED", + "product": "DIY light", + "btype": "bin", + "mac": "aabbccddee11" + }, + "effects": [ + "Solid", + "Blink", + "Breathe", + "Wipe", + "Wipe Random", + "Random Colors", + "Sweep", + "Dynamic", + "Colorloop", + "Rainbow", + "Scan", + "Dual Scan", + "Fade", + "Chase", + "Chase Rainbow", + "Running", + "Saw", + "Twinkle", + "Dissolve", + "Dissolve Rnd", + "Sparkle", + "Dark Sparkle", + "Sparkle+", + "Strobe", + "Strobe Rainbow", + "Mega Strobe", + "Blink Rainbow", + "Android", + "Chase", + "Chase Random", + "Chase Rainbow", + "Chase Flash", + "Chase Flash Rnd", + "Rainbow Runner", + "Colorful", + "Traffic Light", + "Sweep Random", + "Running 2", + "Red & Blue", + "Stream", + "Scanner", + "Lighthouse", + "Fireworks", + "Rain", + "Merry Christmas", + "Fire Flicker", + "Gradient", + "Loading", + "In Out", + "In In", + "Out Out", + "Out In", + "Circus", + "Halloween", + "Tri Chase", + "Tri Wipe", + "Tri Fade", + "Lightning", + "ICU", + "Multi Comet", + "Dual Scanner", + "Stream 2", + "Oscillate", + "Pride 2015", + "Juggle", + "Palette", + "Fire 2012", + "Colorwaves", + "BPM", + "Fill Noise", + "Noise 1", + "Noise 2", + "Noise 3", + "Noise 4", + "Colortwinkles", + "Lake", + "Meteor", + "Smooth Meteor", + "Railway", + "Ripple", + "Twinklefox", + "Twinklecat", + "Halloween Eyes" + ], + "palettes": [ + "Default", + "Random Cycle", + "Primary Color", + "Based on Primary", + "Set Colors", + "Based on Set", + "Party", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Rainbow", + "Rainbow Bands", + "Sunset", + "Rivendell", + "Breeze", + "Red & Blue", + "Yellowout", + "Analogous", + "Splash", + "Pastel", + "Sunset 2", + "Beech", + "Vintage", + "Departure", + "Landscape", + "Beach", + "Sherbet", + "Hult", + "Hult 64", + "Drywet", + "Jul", + "Grintage", + "Rewhi", + "Tertiary", + "Fire", + "Icefire", + "Cyane", + "Light Pink", + "Autumn", + "Magenta", + "Magred", + "Yelmag", + "Yelblu", + "Orange & Teal", + "Tiamat", + "April Night", + "Orangery", + "C9", + "Sakura" + ] +} From 265c390b653a604347f9beaf6e97069e881f86fd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 7 Nov 2019 00:32:16 +0000 Subject: [PATCH 201/306] [ci skip] Translation update --- .../alarm_control_panel/.translations/nl.json | 3 ++ .../components/almond/.translations/nl.json | 10 ++++ .../components/almond/.translations/ru.json | 3 +- .../coolmaster/.translations/nl.json | 13 +++++ .../components/cover/.translations/lb.json | 8 ++++ .../components/cover/.translations/nl.json | 12 ++++- .../components/deconz/.translations/ca.json | 8 ++++ .../components/deconz/.translations/en.json | 18 ++++++- .../components/deconz/.translations/lb.json | 18 ++++++- .../components/deconz/.translations/nl.json | 18 ++++++- .../components/deconz/.translations/pl.json | 18 ++++++- .../components/deconz/.translations/ru.json | 38 ++++++++++----- .../deconz/.translations/zh-Hant.json | 18 ++++++- .../device_tracker/.translations/nl.json | 8 ++++ .../huawei_lte/.translations/ca.json | 5 +- .../huawei_lte/.translations/en.json | 5 +- .../huawei_lte/.translations/fr.json | 5 +- .../huawei_lte/.translations/lb.json | 5 +- .../huawei_lte/.translations/nl.json | 34 ++++++++++++- .../huawei_lte/.translations/pl.json | 5 +- .../huawei_lte/.translations/ru.json | 4 +- .../huawei_lte/.translations/zh-Hant.json | 9 ++-- .../media_player/.translations/nl.json | 11 +++++ .../components/somfy/.translations/nl.json | 5 ++ .../transmission/.translations/nl.json | 5 +- .../components/withings/.translations/nl.json | 4 +- .../components/wled/.translations/en.json | 48 +++++++++---------- .../components/zha/.translations/ru.json | 18 +++---- 28 files changed, 293 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/almond/.translations/nl.json create mode 100644 homeassistant/components/coolmaster/.translations/nl.json create mode 100644 homeassistant/components/device_tracker/.translations/nl.json create mode 100644 homeassistant/components/media_player/.translations/nl.json diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json index 86cacad9fd6..9329a089d32 100644 --- a/homeassistant/components/alarm_control_panel/.translations/nl.json +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -1,6 +1,9 @@ { "device_automation": { "action_type": { + "arm_away": "Inschakelen {entity_name} afwezig", + "arm_home": "Inschakelen {entity_name} thuis", + "arm_night": "Inschakelen {entity_name} nacht", "disarm": "Uitschakelen {entity_name}", "trigger": "Trigger {entity_name}" } diff --git a/homeassistant/components/almond/.translations/nl.json b/homeassistant/components/almond/.translations/nl.json new file mode 100644 index 00000000000..dfe9c238db7 --- /dev/null +++ b/homeassistant/components/almond/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n Almond-account configureren.", + "cannot_connect": "Kan geen verbinding maken met de Almond-server.", + "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ru.json b/homeassistant/components/almond/.translations/ru.json index b513d5b28d7..d15e9a1eeb4 100644 --- a/homeassistant/components/almond/.translations/ru.json +++ b/homeassistant/components/almond/.translations/ru.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "\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.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond.", + "missing_configuration": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond." }, "title": "Almond" } diff --git a/homeassistant/components/coolmaster/.translations/nl.json b/homeassistant/components/coolmaster/.translations/nl.json new file mode 100644 index 00000000000..79a1e9fe1e6 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "error": { + "connection_error": "Kan geen verbinding maken met CoolMasterNet-instantie. Controleer uw host" + }, + "step": { + "user": { + "title": "Stel uw CoolMasterNet-verbindingsgegevens in." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json index 4f7a898c772..b2645f3e001 100644 --- a/homeassistant/components/cover/.translations/lb.json +++ b/homeassistant/components/cover/.translations/lb.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} g\u00ebtt opgemaach", "is_position": "{entity_name} positioun ass", "is_tilt_position": "{entity_name} kipp positioun ass" + }, + "trigger_type": { + "closed": "{entity_name} gouf zougemaach", + "closing": "{entity_name} mecht zou", + "opened": "{entity_name} gouf opgemaach", + "opening": "{entity_name} mecht op", + "position": "{entity_name} positioun \u00e4nnert", + "tilt_position": "{entity_name} kipp positioun ge\u00e4nnert" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/nl.json b/homeassistant/components/cover/.translations/nl.json index 93015afbfdd..472583687dd 100644 --- a/homeassistant/components/cover/.translations/nl.json +++ b/homeassistant/components/cover/.translations/nl.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} is gesloten", "is_closing": "{entity_name} wordt gesloten", "is_open": "{entity_name} is open", - "is_opening": "{entity_name} wordt geopend" + "is_opening": "{entity_name} wordt geopend", + "is_position": "Huidige {entity_name} positie is", + "is_tilt_position": "Huidige {entity_name} kantel positie is" + }, + "trigger_type": { + "closed": "{entity_name} gesloten", + "closing": "{entity_name} wordt gesloten", + "opened": "{entity_name} geopend", + "opening": "{entity_name} wordt geopend", + "position": "{entity_name} positiewijzigingen", + "tilt_position": "{entity_name} kantel positiewijzigingen" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 2900128eedb..676641d38c6 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -55,10 +55,17 @@ "left": "Esquerra", "open": "Obert", "right": "Dreta", + "side_1": "Costat 1", + "side_2": "Costat 2", + "side_3": "Costat 3", + "side_4": "Costat 4", + "side_5": "Costat 5", + "side_6": "Costat 6", "turn_off": "Desactiva", "turn_on": "Activa" }, "trigger_type": { + "remote_awakened": "Dispositiu despertat", "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", @@ -69,6 +76,7 @@ "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", + "remote_falling": "Dispositiu en caiguda lliure", "remote_gyro_activated": "Dispositiu sacsejat" } }, diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index e9c64ffe5fa..63798bab52b 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -55,10 +55,17 @@ "left": "Left", "open": "Open", "right": "Right", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6", "turn_off": "Turn off", "turn_on": "Turn on" }, "trigger_type": { + "remote_awakened": "Device awakened", "remote_button_double_press": "\"{subtype}\" button double clicked", "remote_button_long_press": "\"{subtype}\" button continuously pressed", "remote_button_long_release": "\"{subtype}\" button released after long press", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_gyro_activated": "Device shaken" + "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_falling": "Device in free fall", + "remote_gyro_activated": "Device shaken", + "remote_moved": "Device moved with \"{subtype}\" up", + "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", + "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", + "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", + "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", + "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 49394eb9773..07f88732c62 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -55,10 +55,17 @@ "left": "L\u00e9nks", "open": "Op", "right": "Riets", + "side_1": "S\u00e4it 1", + "side_2": "S\u00e4it 2", + "side_3": "S\u00e4it 3", + "side_4": "S\u00e4it 4", + "side_5": "S\u00e4it 5", + "side_6": "S\u00e4it 6", "turn_off": "Ausschalten", "turn_on": "Uschalten" }, "trigger_type": { + "remote_awakened": "Apparat erw\u00e4cht", "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt", "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt", "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", - "remote_gyro_activated": "Apparat ger\u00ebselt" + "remote_double_tap": "Apparat \"{subtype}\" zwee mol gedr\u00e9ckt", + "remote_falling": "Apparat am fr\u00e4ie Fall", + "remote_gyro_activated": "Apparat ger\u00ebselt", + "remote_moved": "Apparat beweegt mat \"{subtype}\" erop", + "remote_rotate_from_side_1": "Apparat rot\u00e9iert vun der \"S\u00e4it 1\" op \"{subtype}\"", + "remote_rotate_from_side_2": "Apparat rot\u00e9iert vun der \"S\u00e4it 2\" op \"{subtype}\"", + "remote_rotate_from_side_3": "Apparat rot\u00e9iert vun der \"S\u00e4it 3\" op \"{subtype}\"", + "remote_rotate_from_side_4": "Apparat rot\u00e9iert vun der \"S\u00e4it 4\" op \"{subtype}\"", + "remote_rotate_from_side_5": "Apparat rot\u00e9iert vun der \"S\u00e4it 5\" op \"{subtype}\"", + "remote_rotate_from_side_6": "Apparat rot\u00e9iert vun der \"S\u00e4it\" 6 op \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 7f690f11f1d..c0ee391b0c7 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -55,10 +55,17 @@ "left": "Links", "open": "Open", "right": "Rechts", + "side_1": "Zijde 1", + "side_2": "Zijde 2", + "side_3": "Zijde 3", + "side_4": "Zijde 4", + "side_5": "Zijde 5", + "side_6": "Zijde 6", "turn_off": "Uitschakelen", "turn_on": "Inschakelen" }, "trigger_type": { + "remote_awakened": "Apparaat is gewekt", "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", @@ -69,7 +76,16 @@ "remote_button_short_press": "\" {subtype} \" knop ingedrukt", "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", - "remote_gyro_activated": "Apparaat geschud" + "remote_double_tap": "Apparaat \"{subtype}\" dubbel getikt", + "remote_falling": "Apparaat in vrije val", + "remote_gyro_activated": "Apparaat geschud", + "remote_moved": "Apparaat verplaatst met \"{subtype}\" omhoog", + "remote_rotate_from_side_1": "Apparaat gedraaid van \"zijde 1\" naar \"{subtype}\"\".", + "remote_rotate_from_side_2": "Apparaat gedraaid van \"zijde 2\" naar \"{subtype}\"\".", + "remote_rotate_from_side_3": "Apparaat gedraaid van \"zijde 3\" naar \" {subtype} \"", + "remote_rotate_from_side_4": "Apparaat gedraaid van \"zijde 4\" naar \" {subtype} \"", + "remote_rotate_from_side_5": "Apparaat gedraaid van \"zijde 5\" naar \" {subtype} \"", + "remote_rotate_from_side_6": "Apparaat gedraaid van \"zijde 6\" naar \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index ac9f06f1f17..eafecf87d03 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -55,10 +55,17 @@ "left": "w lewo", "open": "otwarcie", "right": "w prawo", + "side_1": "strona 1", + "side_2": "strona 2", + "side_3": "strona 3", + "side_4": "strona 4", + "side_5": "strona 5", + "side_6": "strona 6", "turn_off": "nast\u0105pi wy\u0142\u0105czenie", "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, "trigger_type": { + "remote_awakened": "urz\u0105dzenie si\u0119 obudzi", "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", @@ -69,7 +76,16 @@ "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" + "remote_double_tap": "urz\u0105dzenie \"{subtype}\" zostanie dwukrotnie pukni\u0119te", + "remote_falling": "urz\u0105dzenie zarejestruje swobodny spadek", + "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "remote_moved": "urz\u0105dzenie poruszone z \"{subtype}\" w g\u00f3r\u0119", + "remote_rotate_from_side_1": "urz\u0105dzenie obr\u00f3cone ze \"strona 1\" na \"{subtype}\"", + "remote_rotate_from_side_2": "urz\u0105dzenie obr\u00f3cone ze \"strona 2\" na \"{subtype}\"", + "remote_rotate_from_side_3": "urz\u0105dzenie obr\u00f3cone ze \"strona 3\" na \"{subtype}\"", + "remote_rotate_from_side_4": "urz\u0105dzenie obr\u00f3cone ze \"strona 4\" na \"{subtype}\"", + "remote_rotate_from_side_5": "urz\u0105dzenie obr\u00f3cone ze \"strona 5\" na \"{subtype}\"", + "remote_rotate_from_side_6": "urz\u0105dzenie obr\u00f3cone ze \"strona 6\" na \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 2dc3df17aa9..f0398e530bc 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -55,21 +55,37 @@ "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", + "side_1": "\u0413\u0440\u0430\u043d\u044c 1", + "side_2": "\u0413\u0440\u0430\u043d\u044c 2", + "side_3": "\u0413\u0440\u0430\u043d\u044c 3", + "side_4": "\u0413\u0440\u0430\u043d\u044c 4", + "side_5": "\u0413\u0440\u0430\u043d\u044c 5", + "side_6": "\u0413\u0440\u0430\u043d\u044c 6", "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" }, "trigger_type": { - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", - "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", - "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", - "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", - "remote_button_rotation_stopped": "\u041f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \"{subtype}\"", - "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", - "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438" + "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043b\u0438", + "remote_button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_rotated": "\"{subtype}\" \u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f", + "remote_button_rotation_stopped": "\"{subtype}\" \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u043b\u0430 \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435", + "remote_button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", + "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \"{subtype}\" \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u043c \u043f\u0430\u0434\u0435\u043d\u0438\u0438", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", + "remote_moved": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438, \u043a\u043e\u0433\u0434\u0430 \"{subtype}\" \u0441\u0432\u0435\u0440\u0445\u0443", + "remote_rotate_from_side_1": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 1 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_2": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 2 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_3": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 3 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_4": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 4 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_5": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 5 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 6 \u043d\u0430 \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 975600a5745..0a0e40a8d1e 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -55,10 +55,17 @@ "left": "\u5de6", "open": "\u958b\u555f", "right": "\u53f3", + "side_1": "\u7b2c 1 \u9762", + "side_2": "\u7b2c 2 \u9762", + "side_3": "\u7b2c 3 \u9762", + "side_4": "\u7b2c 4 \u9762", + "side_5": "\u7b2c 5 \u9762", + "side_6": "\u7b2c 6 \u9762", "turn_off": "\u95dc\u9589", "turn_on": "\u958b\u555f" }, "trigger_type": { + "remote_awakened": "\u8a2d\u5099\u5df2\u559a\u9192", "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", - "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643" + "remote_double_tap": "\u8a2d\u5099 \"{subtype}\" \u96d9\u6572", + "remote_falling": "\u8a2d\u5099\u81ea\u7531\u843d\u4e0b", + "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643", + "remote_moved": "\u8a2d\u5099\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", + "remote_rotate_from_side_1": "\u8a2d\u5099\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_2": "\u8a2d\u5099\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_3": "\u8a2d\u5099\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_4": "\u8a2d\u5099\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_5": "\u8a2d\u5099\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/nl.json b/homeassistant/components/device_tracker/.translations/nl.json new file mode 100644 index 00000000000..d4de8b1f66a --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/nl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} is thuis", + "is_not_home": "{entity_name} is niet thuis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index 6dc88f21323..b213da018d2 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Aquest dispositiu ja est\u00e0 configurat" + "already_configured": "Aquest dispositiu ja est\u00e0 configurat", + "already_in_progress": "Aquest dispositiu ja s'est\u00e0 configurant", + "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE" }, "error": { "connection_failed": "La connexi\u00f3 ha fallat", + "connection_timeout": "S'ha acabat el temps d'espera de la connexi\u00f3", "incorrect_password": "Contrasenya incorrecta", "incorrect_username": "Nom d'usuari incorrecte", "incorrect_username_or_password": "Nom d'usuari o contrasenya incorrectes", diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 8681e3355a4..52aaafe595c 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "This device is already configured" + "already_configured": "This device has already been configured", + "already_in_progress": "This device is already being configured", + "not_huawei_lte": "Not a Huawei LTE device" }, "error": { "connection_failed": "Connection failed", + "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index e0394d525d4..5effea3d003 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Ce p\u00e9riph\u00e9rique est d\u00e9j\u00e0 en cours de configuration", + "not_huawei_lte": "Pas un appareil Huawei LTE" }, "error": { "connection_failed": "La connexion a \u00e9chou\u00e9", + "connection_timeout": "D\u00e9lai de connection d\u00e9pass\u00e9", "incorrect_password": "Mot de passe incorrect", "incorrect_username": "Nom d'utilisateur incorrect", "incorrect_username_or_password": "identifiant ou mot de passe incorrect", diff --git a/homeassistant/components/huawei_lte/.translations/lb.json b/homeassistant/components/huawei_lte/.translations/lb.json index 2b90245e929..3c8f0464a55 100644 --- a/homeassistant/components/huawei_lte/.translations/lb.json +++ b/homeassistant/components/huawei_lte/.translations/lb.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "D\u00ebsen Apparat ass scho konfigur\u00e9iert" + "already_configured": "D\u00ebsen Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "D\u00ebsen Apparat g\u00ebtt scho konfigur\u00e9iert", + "not_huawei_lte": "Ken Huawei LTE Apparat" }, "error": { "connection_failed": "Feeler bei der Verbindung", + "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen", "incorrect_password": "Ong\u00ebltegt Passwuert", "incorrect_username": "Ong\u00ebltege Benotzernumm", "incorrect_username_or_password": "Ong\u00ebltege Benotzernumm oder Passwuert", diff --git a/homeassistant/components/huawei_lte/.translations/nl.json b/homeassistant/components/huawei_lte/.translations/nl.json index 4e4b63e9391..6d5e5c3e957 100644 --- a/homeassistant/components/huawei_lte/.translations/nl.json +++ b/homeassistant/components/huawei_lte/.translations/nl.json @@ -1,10 +1,42 @@ { "config": { + "abort": { + "already_configured": "Dit apparaat is reeds geconfigureerd", + "already_in_progress": "Dit apparaat wordt al geconfigureerd", + "not_huawei_lte": "Geen Huawei LTE-apparaat" + }, "error": { + "connection_failed": "Verbinding mislukt", + "connection_timeout": "Time-out van de verbinding", "incorrect_password": "Onjuist wachtwoord", "incorrect_username": "Onjuiste gebruikersnaam", "incorrect_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", - "invalid_url": "Ongeldige URL" + "invalid_url": "Ongeldige URL", + "login_attempts_exceeded": "Maximale aanmeldingspogingen overschreden, probeer het later opnieuw.", + "response_error": "Onbekende fout van het apparaat", + "unknown_connection_error": "Onbekende fout bij verbinden met apparaat" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "url": "URL", + "username": "Gebruikersnaam" + }, + "description": "Voer de toegangsgegevens van het apparaat in. Opgeven van gebruikersnaam en wachtwoord is optioneel, maar biedt ondersteuning voor meer integratiefuncties. Aan de andere kant kan het gebruik van een geautoriseerde verbinding problemen veroorzaken bij het openen van het webinterface van het apparaat buiten de Home Assitant, terwijl de integratie actief is en andersom.", + "title": "Configureer Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Ontvangers van sms-berichten", + "track_new_devices": "Volg nieuwe apparaten" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index 5a8c4033436..3851d0a409f 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE" }, "error": { "connection_failed": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", + "connection_timeout": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia.", "incorrect_password": "Nieprawid\u0142owe has\u0142o", "incorrect_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "incorrect_username_or_password": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index 1a0c5cc29ad..e64018b2a3c 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "connection_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "incorrect_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", "incorrect_username_or_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", diff --git a/homeassistant/components/huawei_lte/.translations/zh-Hant.json b/homeassistant/components/huawei_lte/.translations/zh-Hant.json index 795df4e3d6f..37f1111b77f 100644 --- a/homeassistant/components/huawei_lte/.translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/.translations/zh-Hant.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u8a2d\u5099" }, "error": { "connection_failed": "\u9023\u7dda\u5931\u6557", + "connection_timeout": "\u9023\u7dda\u903e\u6642", "incorrect_password": "\u5bc6\u78bc\u932f\u8aa4", "incorrect_username": "\u4f7f\u7528\u8005\u540d\u7a31\u932f\u8aa4", "incorrect_username_or_password": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4", @@ -21,10 +24,10 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8f38\u5165\u8a2d\u5099\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u8a2d\u5099 Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", - "title": "\u8a2d\u5b9a Huawei LTE" + "title": "\u8a2d\u5b9a\u83ef\u70ba LTE" } }, - "title": "Huawei LTE" + "title": "\u83ef\u70ba LTE" }, "options": { "step": { diff --git a/homeassistant/components/media_player/.translations/nl.json b/homeassistant/components/media_player/.translations/nl.json new file mode 100644 index 00000000000..cfd63d190c3 --- /dev/null +++ b/homeassistant/components/media_player/.translations/nl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} is niet actief", + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", + "is_paused": "{entity_name} is gepauzeerd", + "is_playing": "{entity_name} wordt afgespeeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/nl.json b/homeassistant/components/somfy/.translations/nl.json index be50b280c17..b08bc3431cd 100644 --- a/homeassistant/components/somfy/.translations/nl.json +++ b/homeassistant/components/somfy/.translations/nl.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Succesvol geverifieerd met Somfy." }, + "step": { + "pick_implementation": { + "title": "Kies de authenticatie methode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json index fdf3db99ed0..ccb9c569562 100644 --- a/homeassistant/components/transmission/.translations/nl.json +++ b/homeassistant/components/transmission/.translations/nl.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Host is al geconfigureerd.", "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." }, "error": { "cannot_connect": "Kan geen verbinding maken met host", + "name_exists": "Naam bestaat al", "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Update frequentie" }, - "description": "Configureer opties voor Transmission" + "description": "Configureer opties voor Transmission", + "title": "Configureer de opties voor Transmission" } } } diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index 2ca98656db7..c831561a439 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -10,7 +10,9 @@ "profile": { "data": { "profile": "Profiel" - } + }, + "description": "Welk profiel hebt u op de website van Withings selecteren? Het is belangrijk dat de profielen overeenkomen, anders worden gegevens verkeerd gelabeld.", + "title": "Gebruikersprofiel." }, "user": { "data": { diff --git a/homeassistant/components/wled/.translations/en.json b/homeassistant/components/wled/.translations/en.json index dde66b8e122..0271f7d2b1e 100644 --- a/homeassistant/components/wled/.translations/en.json +++ b/homeassistant/components/wled/.translations/en.json @@ -1,26 +1,26 @@ { - "config": { - "title": "WLED", - "flow_title": "WLED: {name}", - "step": { - "user": { - "title": "Link your WLED", - "description": "Set up your WLED to integrate with Home Assistant.", - "data": { - "host": "Host or IP address" - } - }, - "zeroconf_confirm": { - "description": "Do you want to add the WLED named `{name}` to Home Assistant?", - "title": "Discovered WLED device" - } - }, - "error": { - "connection_error": "Failed to connect to WLED device." - }, - "abort": { - "already_configured": "This WLED device is already configured.", - "connection_error": "Failed to connect to WLED device." + "config": { + "abort": { + "already_configured": "This WLED device is already configured.", + "connection_error": "Failed to connect to WLED device." + }, + "error": { + "connection_error": "Failed to connect to WLED device." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Host or IP address" + }, + "description": "Set up your WLED to integrate with Home Assistant.", + "title": "Link your WLED" + }, + "zeroconf_confirm": { + "description": "Do you want to add the WLED named `{name}` to Home Assistant?", + "title": "Discovered WLED device" + } + }, + "title": "WLED" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 983a69cdc2c..c0bc7c176a2 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -49,19 +49,19 @@ "trigger_type": { "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u043b\u0438", "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", - "device_knocked": "\u041f\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\"", + "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\" ", "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", "device_slid": "\u0421\u0434\u0432\u0438\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \"{subtype}\"", "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u043a\u043b\u043e\u043d\u0438\u043b\u0438", - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", - "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", - "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", - "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" + "remote_button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } } } \ No newline at end of file From b5587348f5db495d24ad58d06bd78e2c97bfef6b Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 7 Nov 2019 16:37:29 +1100 Subject: [PATCH 202/306] update to latest integration library version (#28597) --- homeassistant/components/geonetnz_quakes/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index f7aa53b0a3a..9996e1d1cb3 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", "requirements": [ - "aio_geojson_geonetnz_quakes==0.10" + "aio_geojson_geonetnz_quakes==0.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ac6529ff869..a27f5aa4af2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,7 +124,7 @@ adguardhome==0.3.0 afsapi==0.0.4 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.10 +aio_geojson_geonetnz_quakes==0.11 # homeassistant.components.ambient_station aioambient==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 605e244ace1..186d2576f72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ adb-shell==0.0.8 adguardhome==0.3.0 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.10 +aio_geojson_geonetnz_quakes==0.11 # homeassistant.components.ambient_station aioambient==0.3.2 From 68fd39e3215154d4cb56255770879308ba244f54 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Thu, 7 Nov 2019 01:42:42 -0800 Subject: [PATCH 203/306] Upgrade greeneye_monitor to 1.0.1 (#28600) This release has a fix for a crash we were seeing occasionally, and a totally revamped packet handler which should be more robust and CPU-efficient. Fixes #25887 --- homeassistant/components/greeneye_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index 1e9569e8509..eb5f19bc1ee 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -3,7 +3,7 @@ "name": "Greeneye monitor", "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": [ - "greeneye_monitor==1.0" + "greeneye_monitor==1.0.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index a27f5aa4af2..ca1ae68e4a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -595,7 +595,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.greeneye_monitor -greeneye_monitor==1.0 +greeneye_monitor==1.0.1 # homeassistant.components.greenwave greenwavereality==0.5.1 From 11cdce3758fab5a87c755abcae284035e8315cae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 03:24:58 -0800 Subject: [PATCH 204/306] Add device actions to vacuum (#28554) --- .../components/vacuum/device_action.py | 72 ++++++++++++++ homeassistant/components/vacuum/strings.json | 8 ++ tests/components/vacuum/test_device_action.py | 98 +++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 homeassistant/components/vacuum/device_action.py create mode 100644 homeassistant/components/vacuum/strings.json create mode 100644 tests/components/vacuum/test_device_action.py diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py new file mode 100644 index 00000000000..e5f8c162fbd --- /dev/null +++ b/homeassistant/components/vacuum/device_action.py @@ -0,0 +1,72 @@ +"""Provides device automations for Vacuum.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN, SERVICE_START, SERVICE_RETURN_TO_BASE + +ACTION_TYPES = {"clean", "dock"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Vacuum devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "clean", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "dock", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "clean": + service = SERVICE_START + elif config[CONF_TYPE] == "dock": + service = SERVICE_RETURN_TO_BASE + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json new file mode 100644 index 00000000000..461af9d9794 --- /dev/null +++ b/homeassistant/components/vacuum/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "clean": "Let {entity_name} clean", + "dock": "Let {entity_name} return to the dock" + } + } +} diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py new file mode 100644 index 00000000000..a7c0859408f --- /dev/null +++ b/tests/components/vacuum/test_device_action.py @@ -0,0 +1,98 @@ +"""The tests for Vacuum device actions.""" +import pytest + +from homeassistant.components.vacuum import DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a vacuum.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "clean", + "device_id": device_entry.id, + "entity_id": "vacuum.test_5678", + }, + { + "domain": DOMAIN, + "type": "dock", + "device_id": device_entry.id, + "entity_id": "vacuum.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event_dock"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "vacuum.entity", + "type": "dock", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_clean"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "vacuum.entity", + "type": "clean", + }, + }, + ] + }, + ) + + dock_calls = async_mock_service(hass, "vacuum", "return_to_base") + clean_calls = async_mock_service(hass, "vacuum", "start") + + hass.bus.async_fire("test_event_dock") + await hass.async_block_till_done() + assert len(dock_calls) == 1 + assert len(clean_calls) == 0 + + hass.bus.async_fire("test_event_clean") + await hass.async_block_till_done() + assert len(dock_calls) == 1 + assert len(clean_calls) == 1 From 2c607c836f0d12e6175013b555a7b91d9c0273b3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 03:26:10 -0800 Subject: [PATCH 205/306] Add device action to fan (#28550) --- homeassistant/components/fan/device_action.py | 74 +++++++++++++ homeassistant/components/fan/strings.json | 8 ++ tests/components/fan/test_device_action.py | 104 ++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 homeassistant/components/fan/device_action.py create mode 100644 homeassistant/components/fan/strings.json create mode 100644 tests/components/fan/test_device_action.py diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py new file mode 100644 index 00000000000..b26f632a775 --- /dev/null +++ b/homeassistant/components/fan/device_action.py @@ -0,0 +1,74 @@ +"""Provides device automations for Fan.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN + +ACTION_TYPES = {"turn_on", "turn_off"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Fan devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_on", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_off", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "turn_on": + service = SERVICE_TURN_ON + elif config[CONF_TYPE] == "turn_off": + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json new file mode 100644 index 00000000000..8f5709ac155 --- /dev/null +++ b/homeassistant/components/fan/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" + } + } +} \ No newline at end of file diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py new file mode 100644 index 00000000000..928fd353dd5 --- /dev/null +++ b/tests/components/fan/test_device_action.py @@ -0,0 +1,104 @@ +"""The tests for Fan device actions.""" +import pytest + +from homeassistant.components.fan import DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a fan.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": "fan.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": "fan.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_off", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "fan.entity", + "type": "turn_off", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_on", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "fan.entity", + "type": "turn_on", + }, + }, + ] + }, + ) + + turn_off_calls = async_mock_service(hass, "fan", "turn_off") + turn_on_calls = async_mock_service(hass, "fan", "turn_on") + + hass.bus.async_fire("test_event_turn_off") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 0 + + hass.bus.async_fire("test_event_turn_on") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 1 From 78657bfbafeafa6a0aad0e71a940f0dd32faf476 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 03:26:59 -0800 Subject: [PATCH 206/306] Add lock device triggers (#28547) * Add lock device triggers * Lint --- .../components/lock/device_trigger.py | 89 ++++++++++++ homeassistant/components/lock/strings.json | 4 + tests/components/lock/test_device_trigger.py | 132 ++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 homeassistant/components/lock/device_trigger.py create mode 100644 tests/components/lock/test_device_trigger.py diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py new file mode 100644 index 00000000000..8732cca29f0 --- /dev/null +++ b/homeassistant/components/lock/device_trigger.py @@ -0,0 +1,89 @@ +"""Provides device automations for Lock.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_LOCKED, + STATE_UNLOCKED, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN + +TRIGGER_TYPES = {"locked", "unlocked"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Lock devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "locked", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "unlocked", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "locked": + from_state = STATE_UNLOCKED + to_state = STATE_LOCKED + else: + from_state = STATE_LOCKED + to_state = STATE_UNLOCKED + + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index 9c858916476..1645b78295d 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked" + }, + "trigger_type": { + "locked": "{entity_name} locked", + "unlocked": "{entity_name} unlocked" } } } diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py new file mode 100644 index 00000000000..572e28c44a6 --- /dev/null +++ b/tests/components/lock/test_device_trigger.py @@ -0,0 +1,132 @@ +"""The tests for Lock device triggers.""" +import pytest + +from homeassistant.components.lock import DOMAIN +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a lock.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "locked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "unlocked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("lock.entity", STATE_UNLOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "locked", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "locked - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "unlocked", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "unlocked - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("lock.entity", STATE_LOCKED) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "locked - device - {} - unlocked - locked - None".format("lock.entity") + + # Fake that the entity is turning off. + hass.states.async_set("lock.entity", STATE_UNLOCKED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data[ + "some" + ] == "unlocked - device - {} - locked - unlocked - None".format("lock.entity") From 5032c5a04c25f5e2b93b55477342e535922b4b07 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 03:38:58 -0800 Subject: [PATCH 207/306] Add fan device trigger (#28545) --- .../components/fan/device_trigger.py | 89 ++++++++++++ homeassistant/components/fan/strings.json | 3 + tests/components/fan/test_device_trigger.py | 132 ++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 homeassistant/components/fan/device_trigger.py create mode 100644 tests/components/fan/test_device_trigger.py diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py new file mode 100644 index 00000000000..3e917e0ae79 --- /dev/null +++ b/homeassistant/components/fan/device_trigger.py @@ -0,0 +1,89 @@ +"""Provides device automations for Fan.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_ON, + STATE_OFF, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN + +TRIGGER_TYPES = {"turned_on", "turned_off"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Fan devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_on", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_off", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "turned_on": + from_state = STATE_OFF + to_state = STATE_ON + else: + from_state = STATE_ON + to_state = STATE_OFF + + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 8f5709ac155..dd1bb715ce7 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -1,5 +1,8 @@ { "device_automation": { + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" "action_type": { "turn_on": "Turn on {entity_name}", "turn_off": "Turn off {entity_name}" diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py new file mode 100644 index 00000000000..fa41749cf36 --- /dev/null +++ b/tests/components/fan/test_device_trigger.py @@ -0,0 +1,132 @@ +"""The tests for Fan device triggers.""" +import pytest + +from homeassistant.components.fan import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a fan.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("fan.entity", STATE_OFF) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_on - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_off - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("fan.entity", STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_on - device - {} - off - on - None".format( + "fan.entity" + ) + + # Fake that the entity is turning off. + hass.states.async_set("fan.entity", STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_off - device - {} - on - off - None".format( + "fan.entity" + ) From c3f07347a199a6ba7ac1392e55c7d8d13650f2bc Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Thu, 7 Nov 2019 22:39:16 +1100 Subject: [PATCH 208/306] Fix simple typo: unhasable -> unhashable (#28605) --- tests/util/test_yaml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index c2bf33f9010..1e5797e33e7 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -39,7 +39,7 @@ def test_simple_dict(): def test_unhashable_key(): - """Test an unhasable key.""" + """Test an unhashable key.""" files = {YAML_CONFIG_FILE: "message:\n {{ states.state }}"} with pytest.raises(HomeAssistantError), patch_yaml_files(files): load_yaml_config_file(YAML_CONFIG_FILE) From 6999a712ef8262736ec03932519a4324e31fd6b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 04:39:35 -0800 Subject: [PATCH 209/306] Add device triggers to vacuum (#28548) * Add device triggers to vacuum * Update strings --- homeassistant/components/automation/state.py | 4 +- homeassistant/components/vacuum/__init__.py | 2 + .../components/vacuum/device_trigger.py | 86 ++++++++++++ homeassistant/components/vacuum/strings.json | 3 + .../components/vacuum/test_device_trigger.py | 131 ++++++++++++++++++ 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/vacuum/device_trigger.py create mode 100644 tests/components/vacuum/test_device_trigger.py diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 154394075a0..47c44587b08 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -27,8 +27,8 @@ TRIGGER_SCHEMA = vol.All( vol.Required(CONF_PLATFORM): "state", vol.Required(CONF_ENTITY_ID): cv.entity_ids, # These are str on purpose. Want to catch YAML conversions - vol.Optional(CONF_FROM): str, - vol.Optional(CONF_TO): str, + vol.Optional(CONF_FROM): vol.Any(str, [str]), + vol.Optional(CONF_TO): vol.Any(str, [str]), vol.Optional(CONF_FOR): vol.Any( vol.All(cv.time_period, cv.positive_timedelta), cv.template, diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 55e56415b0d..ace3f610606 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -71,6 +71,8 @@ STATE_DOCKED = "docked" STATE_RETURNING = "returning" STATE_ERROR = "error" +STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR] + DEFAULT_NAME = "Vacuum cleaner robot" SUPPORT_TURN_ON = 1 diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py new file mode 100644 index 00000000000..328db54b1b9 --- /dev/null +++ b/homeassistant/components/vacuum/device_trigger.py @@ -0,0 +1,86 @@ +"""Provides device automations for Vacuum.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATES + +TRIGGER_TYPES = {"cleaning", "docked"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Vacuum devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "cleaning", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "docked", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "cleaning": + from_state = [state for state in STATES if state != STATE_CLEANING] + to_state = STATE_CLEANING + else: + from_state = [state for state in STATES if state != STATE_DOCKED] + to_state = STATE_DOCKED + + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 461af9d9794..5fca1be5197 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -1,5 +1,8 @@ { "device_automation": { + "trigger_type": { + "cleaning": "{entity_name} started cleaning", + "docked": "{entity_name} docked" "action_type": { "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py new file mode 100644 index 00000000000..680b6482186 --- /dev/null +++ b/tests/components/vacuum/test_device_trigger.py @@ -0,0 +1,131 @@ +"""The tests for Vacuum device triggers.""" +import pytest + +from homeassistant.components.vacuum import DOMAIN, STATE_DOCKED, STATE_CLEANING +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a vacuum.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "cleaning", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "docked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("vacuum.entity", STATE_DOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "vacuum.entity", + "type": "cleaning", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "cleaning - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "vacuum.entity", + "type": "docked", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "docked - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is cleaning + hass.states.async_set("vacuum.entity", STATE_CLEANING) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "cleaning - device - {} - docked - cleaning".format( + "vacuum.entity" + ) + + # Fake that the entity is docked + hass.states.async_set("vacuum.entity", STATE_DOCKED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "docked - device - {} - cleaning - docked".format( + "vacuum.entity" + ) From 76aae0c23eb4cbc81601f5ec4707c919c5791cd3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Nov 2019 13:43:43 +0100 Subject: [PATCH 210/306] Fix demo TTS (#28608) --- homeassistant/components/demo/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index 27c9015533e..b7be6349d98 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -16,7 +16,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_engine(hass, config, discovery_info=None): """Set up Demo speech component.""" - return DemoProvider(config[CONF_LANG]) + return DemoProvider(config.get(CONF_LANG, DEFAULT_LANG)) class DemoProvider(Provider): From d34caf50f124eee405197665d58994604c8feb73 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 04:44:59 -0800 Subject: [PATCH 211/306] Add climate device actions (#28552) --- .../components/climate/device_action.py | 111 +++++++++++ homeassistant/components/climate/strings.json | 8 + .../components/climate/test_device_action.py | 177 ++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 homeassistant/components/climate/device_action.py create mode 100644 homeassistant/components/climate/strings.json create mode 100644 tests/components/climate/test_device_action.py diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py new file mode 100644 index 00000000000..b53109f69cb --- /dev/null +++ b/homeassistant/components/climate/device_action.py @@ -0,0 +1,111 @@ +"""Provides device automations for Climate.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN, const + +ACTION_TYPES = {"set_hvac_mode", "set_preset_mode"} + +SET_HVAC_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): "set_hvac_mode", + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(const.ATTR_HVAC_MODE): vol.In(const.HVAC_MODES), + } +) + +SET_PRESET_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): "set_preset_mode", + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(const.ATTR_PRESET_MODE): str, + } +) + +ACTION_SCHEMA = vol.Any(SET_HVAC_MODE_SCHEMA, SET_PRESET_MODE_SCHEMA) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Climate devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if state is None: + continue + + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "set_hvac_mode", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "set_preset_mode", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "set_hvac_mode": + service = const.SERVICE_SET_HVAC_MODE + service_data[const.ATTR_HVAC_MODE] = config[const.ATTR_HVAC_MODE] + elif config[CONF_TYPE] == "set_preset_mode": + service = const.SERVICE_SET_PRESET_MODE + service_data[const.ATTR_PRESET_MODE] = config[const.ATTR_PRESET_MODE] + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities(hass, config): + """List action capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + action_type = config[CONF_TYPE] + + fields = {} + + if action_type == "set_hvac_mode": + hvac_modes = state.attributes[const.ATTR_HVAC_MODES] if state else [] + fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes) + elif action_type == "set_preset_mode": + if state: + preset_modes = state.attributes.get(const.ATTR_PRESET_MODES, []) + else: + preset_modes = [] + fields[vol.Required(const.ATTR_PRESET_MODE)] = vol.In(preset_modes) + + return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json new file mode 100644 index 00000000000..7e97c7701c2 --- /dev/null +++ b/homeassistant/components/climate/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Change HVAC mode on {entity_name}", + "set_preset_mode": "Change preset on {entity_name}" + } + } +} diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py new file mode 100644 index 00000000000..3eb1f38ec41 --- /dev/null +++ b/tests/components/climate/test_device_action.py @@ -0,0 +1,177 @@ +"""The tests for Climate device actions.""" +import pytest +import voluptuous_serialize + +from homeassistant.components.climate import DOMAIN, const, device_action +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry, config_validation as cv + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a climate.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) + expected_actions = [ + { + "domain": DOMAIN, + "type": "set_hvac_mode", + "device_id": device_entry.id, + "entity_id": "climate.test_5678", + }, + { + "domain": DOMAIN, + "type": "set_preset_mode", + "device_id": device_entry.id, + "entity_id": "climate.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for actions.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_hvac_mode", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "climate.entity", + "type": "set_hvac_mode", + "hvac_mode": const.HVAC_MODE_OFF, + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_preset_mode", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "climate.entity", + "type": "set_preset_mode", + "preset_mode": const.PRESET_AWAY, + }, + }, + ] + }, + ) + + set_hvac_mode_calls = async_mock_service(hass, "climate", "set_hvac_mode") + set_preset_mode_calls = async_mock_service(hass, "climate", "set_preset_mode") + + hass.bus.async_fire("test_event_set_hvac_mode") + await hass.async_block_till_done() + assert len(set_hvac_mode_calls) == 1 + assert len(set_preset_mode_calls) == 0 + + hass.bus.async_fire("test_event_set_preset_mode") + await hass.async_block_till_done() + assert len(set_hvac_mode_calls) == 1 + assert len(set_preset_mode_calls) == 1 + + +async def test_capabilities(hass): + """Test getting capabilities.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + + # Set HVAC mode + capabilities = await device_action.async_get_action_capabilities( + hass, + { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "climate.entity", + "type": "set_hvac_mode", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ] + + # Set preset mode + capabilities = await device_action.async_get_action_capabilities( + hass, + { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "climate.entity", + "type": "set_preset_mode", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "preset_mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ] From 50f1b6c689cf10513b42f3f094845f982ef71145 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 7 Nov 2019 13:47:26 +0100 Subject: [PATCH 212/306] Fix vacuum strings --- homeassistant/components/vacuum/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 5fca1be5197..2a6fcbbd357 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -3,6 +3,7 @@ "trigger_type": { "cleaning": "{entity_name} started cleaning", "docked": "{entity_name} docked" + }, "action_type": { "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" From abb78a0d132b0cd5072f5b852c171b3c6c0b0b72 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 7 Nov 2019 13:48:23 +0100 Subject: [PATCH 213/306] Fix fan strings --- homeassistant/components/fan/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index dd1bb715ce7..d566c76994c 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -3,9 +3,10 @@ "trigger_type": { "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" + }, "action_type": { "turn_on": "Turn on {entity_name}", "turn_off": "Turn off {entity_name}" } } -} \ No newline at end of file +} From af73e54aee51944c72ce2afd54c90cf617273ab4 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Thu, 7 Nov 2019 14:47:44 +0100 Subject: [PATCH 214/306] Add azure servicebus notify service (#27566) * Add azure servicebus notify service * files added to .coveragerc * fix: import content type from const * Moved imports to top level * Code review fixes + added code owner + fixed config validation with has at least one + seperate attributes for dto to asb * fixed doc link * async setup instead of sync * rename all the things - removed too many ifs * changed setup back to sync + comment about sync IO in lib * More informative logging * logging exception -> error --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/azure_service_bus/__init__.py | 1 + .../azure_service_bus/manifest.json | 12 ++ .../components/azure_service_bus/notify.py | 106 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 124 insertions(+) create mode 100644 homeassistant/components/azure_service_bus/__init__.py create mode 100644 homeassistant/components/azure_service_bus/manifest.json create mode 100644 homeassistant/components/azure_service_bus/notify.py diff --git a/.coveragerc b/.coveragerc index e6f09d60eff..169b73b7899 100644 --- a/.coveragerc +++ b/.coveragerc @@ -69,6 +69,7 @@ omit = homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/azure_event_hub/* + homeassistant/components/azure_service_bus/* homeassistant/components/baidu/tts.py homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbb_gpio/* diff --git a/CODEOWNERS b/CODEOWNERS index 27e06d874e1..0a02fbc5321 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -43,6 +43,7 @@ homeassistant/components/awair/* @danielsjf homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/axis/* @kane610 homeassistant/components/azure_event_hub/* @eavanvalkenburg +homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria diff --git a/homeassistant/components/azure_service_bus/__init__.py b/homeassistant/components/azure_service_bus/__init__.py new file mode 100644 index 00000000000..f18dc9eb66c --- /dev/null +++ b/homeassistant/components/azure_service_bus/__init__.py @@ -0,0 +1 @@ +"""The Azure Service Bus integration.""" diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json new file mode 100644 index 00000000000..fa6d1c20b7f --- /dev/null +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "azure_service_bus", + "name": "Azure Service Bus", + "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", + "requirements": [ + "azure-servicebus==0.50.1" + ], + "dependencies": [], + "codeowners": [ + "@hfurubotten" + ] +} \ No newline at end of file diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py new file mode 100644 index 00000000000..e7c85adede8 --- /dev/null +++ b/homeassistant/components/azure_service_bus/notify.py @@ -0,0 +1,106 @@ +"""Support for azure service bus notification.""" +import json +import logging + +from azure.servicebus.aio import Message, ServiceBusClient +from azure.servicebus.common.errors import ( + MessageSendFailed, + ServiceBusConnectionError, + ServiceBusResourceNotFound, +) +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONTENT_TYPE_JSON +import homeassistant.helpers.config_validation as cv + +CONF_CONNECTION_STRING = "connection_string" +CONF_QUEUE_NAME = "queue" +CONF_TOPIC_NAME = "topic" + +ATTR_ASB_MESSAGE = "message" +ATTR_ASB_TITLE = "title" +ATTR_ASB_TARGET = "target" + +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_QUEUE_NAME, CONF_TOPIC_NAME), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_CONNECTION_STRING): cv.string, + vol.Exclusive( + CONF_QUEUE_NAME, "output", "Can only send to a queue or a topic." + ): cv.string, + vol.Exclusive( + CONF_TOPIC_NAME, "output", "Can only send to a queue or a topic." + ): cv.string, + } + ), +) + +_LOGGER = logging.getLogger(__name__) + + +def get_service(hass, config, discovery_info=None): + """Get the notification service.""" + connection_string = config[CONF_CONNECTION_STRING] + queue_name = config.get(CONF_QUEUE_NAME) + topic_name = config.get(CONF_TOPIC_NAME) + + # Library can do synchronous IO when creating the clients. + # Passes in loop here, but can't run setup on the event loop. + servicebus = ServiceBusClient.from_connection_string( + connection_string, loop=hass.loop + ) + + try: + if queue_name: + client = servicebus.get_queue(queue_name) + else: + client = servicebus.get_topic(topic_name) + except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err: + _LOGGER.error( + "Connection error while creating client for queue/topic '%s'. %s", + queue_name or topic_name, + err, + ) + return None + + return ServiceBusNotificationService(client) + + +class ServiceBusNotificationService(BaseNotificationService): + """Implement the notification service for the service bus service.""" + + def __init__(self, client): + """Initialize the service.""" + self._client = client + + async def async_send_message(self, message, **kwargs): + """Send a message.""" + dto = {ATTR_ASB_MESSAGE: message} + + if ATTR_TITLE in kwargs: + dto[ATTR_ASB_TITLE] = kwargs[ATTR_TITLE] + if ATTR_TARGET in kwargs: + dto[ATTR_ASB_TARGET] = kwargs[ATTR_TARGET] + + data = kwargs.get(ATTR_DATA) + if data: + dto.update(data) + + queue_message = Message(json.dumps(dto)) + queue_message.properties.content_type = CONTENT_TYPE_JSON + try: + await self._client.send(queue_message) + except MessageSendFailed as err: + _LOGGER.error( + "Could not send service bus notification to %s. %s", + self._client.name, + err, + ) diff --git a/requirements_all.txt b/requirements_all.txt index ca1ae68e4a6..8843a6693ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -263,6 +263,9 @@ axis==25 # homeassistant.components.azure_event_hub azure-eventhub==1.3.1 +# homeassistant.components.azure_service_bus +azure-servicebus==0.50.1 + # homeassistant.components.baidu baidu-aip==1.6.6 From 48660585f1d699a7825af5c72e00a6f9cd4419ff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 07:28:45 -0800 Subject: [PATCH 215/306] Add climate device triggers (#28544) * Add climate device triggers * Test capabilities --- .../components/automation/template.py | 6 +- homeassistant/components/climate/__init__.py | 4 +- homeassistant/components/climate/const.py | 2 +- .../components/climate/device_trigger.py | 192 ++++++++++++++ homeassistant/components/climate/strings.json | 5 + .../components/homekit/type_thermostats.py | 4 +- .../components/climate/test_device_trigger.py | 243 ++++++++++++++++++ tests/components/demo/test_climate.py | 6 +- .../homekit/test_type_thermostats.py | 28 +- tests/components/smartthings/test_climate.py | 8 +- 10 files changed, 470 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/climate/device_trigger.py create mode 100644 tests/components/climate/test_device_trigger.py diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index f2b4134de42..95b6b857c9d 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -28,7 +28,9 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="numeric_state" +): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass @@ -65,7 +67,7 @@ async def async_attach_trigger(hass, config, action, automation_info): variables = { "trigger": { - "platform": "template", + "platform": platform_type, "entity_id": entity_id, "from_state": from_s, "to_state": to_s, diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index af67be5eccc..0c8d6103b12 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -35,7 +35,7 @@ from .const import ( ATTR_FAN_MODE, ATTR_FAN_MODES, ATTR_HUMIDITY, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, @@ -239,7 +239,7 @@ class ClimateDevice(Entity): data[ATTR_FAN_MODES] = self.fan_modes if self.hvac_action: - data[ATTR_HVAC_ACTIONS] = self.hvac_action + data[ATTR_HVAC_ACTION] = self.hvac_action if supported_features & SUPPORT_PRESET_MODE: data[ATTR_PRESET_MODE] = self.preset_mode diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 4012aa8be1b..26cec7efbeb 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -97,7 +97,7 @@ ATTR_MAX_HUMIDITY = "max_humidity" ATTR_MIN_HUMIDITY = "min_humidity" ATTR_MAX_TEMP = "max_temp" ATTR_MIN_TEMP = "min_temp" -ATTR_HVAC_ACTIONS = "hvac_action" +ATTR_HVAC_ACTION = "hvac_action" ATTR_HVAC_MODES = "hvac_modes" ATTR_HVAC_MODE = "hvac_mode" ATTR_SWING_MODES = "swing_modes" diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py new file mode 100644 index 00000000000..e814bdc88de --- /dev/null +++ b/homeassistant/components/climate/device_trigger.py @@ -0,0 +1,192 @@ +"""Provides device automations for Climate.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + CONF_FOR, + CONF_ABOVE, + CONF_BELOW, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import ( + state as state_automation, + numeric_state as numeric_state_automation, + AutomationActionType, +) +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN, const + +TRIGGER_TYPES = { + "current_temperature_changed", + "current_humidity_changed", + "hvac_mode_changed", +} + +HVAC_MODE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): "hvac_mode_changed", + vol.Required(state_automation.CONF_TO): vol.In(const.HVAC_MODES), + } +) + +CURRENT_TRIGGER_SCHEMA = vol.All( + TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + ["current_temperature_changed", "current_humidity_changed"] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + +TRIGGER_SCHEMA = vol.Any(HVAC_MODE_TRIGGER_SCHEMA, CURRENT_TRIGGER_SCHEMA) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Climate devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + # Add triggers for each entity that belongs to this integration + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "hvac_mode_changed", + } + ) + + if state and const.ATTR_CURRENT_TEMPERATURE in state.attributes: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "current_temperature_changed", + } + ) + + if state and const.ATTR_CURRENT_HUMIDITY in state.attributes: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "current_humidity_changed", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + trigger_type = config[CONF_TYPE] + + if trigger_type == "hvac_mode_changed": + state_config = { + state_automation.CONF_PLATFORM: "state", + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_TO: config[state_automation.CONF_TO], + state_automation.CONF_FROM: [ + mode + for mode in const.HVAC_MODES + if mode != config[state_automation.CONF_TO] + ], + } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + state_config = state_automation.TRIGGER_SCHEMA(state_config) + return await state_automation.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + numeric_state_config = { + numeric_state_automation.CONF_PLATFORM: "numeric_state", + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + + if trigger_type == "current_temperature_changed": + numeric_state_config[ + numeric_state_automation.CONF_VALUE_TEMPLATE + ] = "{{ state.attributes.current_temperature }}" + else: + numeric_state_config[ + numeric_state_automation.CONF_VALUE_TEMPLATE + ] = "{{ state.attributes.current_humidity }}" + + if CONF_ABOVE in config: + numeric_state_config[CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[CONF_BELOW] = config[CONF_BELOW] + if CONF_FOR in config: + numeric_state_config[CONF_FOR] = config[CONF_FOR] + + numeric_state_config = numeric_state_automation.TRIGGER_SCHEMA(numeric_state_config) + return await numeric_state_automation.async_attach_trigger( + hass, numeric_state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config): + """List trigger capabilities.""" + trigger_type = config[CONF_TYPE] + + if trigger_type == "hvac_action_changed": + return None + + if trigger_type == "hvac_mode_changed": + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + if trigger_type == "current_temperature_changed": + unit_of_measurement = hass.config.units.temperature_unit + else: + unit_of_measurement = "%" + + return { + "extra_fields": vol.Schema( + { + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ) + } diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 7e97c7701c2..84854c713cf 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -1,5 +1,10 @@ { "device_automation": { + "trigger_type": { + "current_temperature_changed": "{entity_name} measured temperature changed", + "current_humidity_changed": "{entity_name} measured humidity changed", + "hvac_mode_changed": "{entity_name} HVAC mode changed" + }, "action_type": { "set_hvac_mode": "Change HVAC mode on {entity_name}", "set_preset_mode": "Change preset on {entity_name}" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 63eb688a0c1..9adc3cc0600 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -5,7 +5,7 @@ from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, @@ -293,7 +293,7 @@ class Thermostat(HomeAccessory): self._flag_heat_cool = False # Set current operation mode for supported thermostats - hvac_action = new_state.attributes.get(ATTR_HVAC_ACTIONS) + hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION) if hvac_action: self.char_current_heat_cool.set_value( HC_HASS_TO_HOMEKIT_ACTION[hvac_action] diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py new file mode 100644 index 00000000000..3b497912c52 --- /dev/null +++ b/tests/components/climate/test_device_trigger.py @@ -0,0 +1,243 @@ +"""The tests for Climate device triggers.""" +import voluptuous_serialize +import pytest + +from homeassistant.components.climate import DOMAIN, const, device_trigger +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry, config_validation as cv + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a climate device.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + entity_id = f"{DOMAIN}.test_5678" + hass.states.async_set( + entity_id, + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_IDLE, + const.ATTR_CURRENT_HUMIDITY: 23, + const.ATTR_CURRENT_TEMPERATURE: 18, + }, + ) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "hvac_mode_changed", + "device_id": device_entry.id, + "entity_id": entity_id, + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "current_temperature_changed", + "device_id": device_entry.id, + "entity_id": entity_id, + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "current_humidity_changed", + "device_id": device_entry.id, + "entity_id": entity_id, + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_IDLE, + const.ATTR_CURRENT_HUMIDITY: 23, + const.ATTR_CURRENT_TEMPERATURE: 18, + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "hvac_mode_changed", + "to": const.HVAC_MODE_AUTO, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "hvac_mode_changed"}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "current_temperature_changed", + "above": 20, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "current_temperature_changed"}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "current_humidity_changed", + "below": 10, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "current_humidity_changed"}, + }, + }, + ] + }, + ) + + # Fake that the HVAC mode is changing + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_COOL, + const.ATTR_CURRENT_HUMIDITY: 23, + const.ATTR_CURRENT_TEMPERATURE: 18, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "hvac_mode_changed" + + # Fake that the temperature is changing + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_COOL, + const.ATTR_CURRENT_HUMIDITY: 23, + const.ATTR_CURRENT_TEMPERATURE: 23, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "current_temperature_changed" + + # Fake that the humidity is changing + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_COOL, + const.ATTR_CURRENT_HUMIDITY: 7, + const.ATTR_CURRENT_TEMPERATURE: 23, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "current_humidity_changed" + + +async def test_get_trigger_capabilities_hvac_mode(hass): + """Test we get the expected capabilities from a climate trigger.""" + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": "climate", + "type": "hvac_mode_changed", + "entity_id": "climate.upstairs", + "to": "heat", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}] + + +@pytest.mark.parametrize( + "type", ["current_temperature_changed", "current_humidity_changed"] +) +async def test_get_trigger_capabilities_temp_humid(hass, type): + """Test we get the expected capabilities from a climate trigger.""" + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": "climate", + "type": "current_temperature_changed", + "entity_id": "climate.upstairs", + "above": "23", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "description": {"suffix": "°C"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"suffix": "°C"}, + "name": "below", + "optional": True, + "type": "float", + }, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + ] diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index eef03b2370a..2d1b7a85ff8 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -9,7 +9,7 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HUMIDITY, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, @@ -233,7 +233,7 @@ async def test_set_hvac_bad_attr_and_state(hass): Also check the state. """ state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_HVAC_ACTIONS) == CURRENT_HVAC_COOL + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_COOL assert state.state == HVAC_MODE_COOL with pytest.raises(vol.Invalid): @@ -241,7 +241,7 @@ async def test_set_hvac_bad_attr_and_state(hass): await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_HVAC_ACTIONS) == CURRENT_HVAC_COOL + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_COOL assert state.state == HVAC_MODE_COOL diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 8ad46e489d6..c896ad211e8 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_TEMP, @@ -92,7 +92,7 @@ async def test_thermostat(hass, hk_driver, cls, events): { ATTR_TEMPERATURE: 22.2, ATTR_CURRENT_TEMPERATURE: 17.8, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, }, ) await hass.async_block_till_done() @@ -108,7 +108,7 @@ async def test_thermostat(hass, hk_driver, cls, events): { ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 23.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -124,7 +124,7 @@ async def test_thermostat(hass, hk_driver, cls, events): { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, }, ) await hass.async_block_till_done() @@ -140,7 +140,7 @@ async def test_thermostat(hass, hk_driver, cls, events): { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 19.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -169,7 +169,7 @@ async def test_thermostat(hass, hk_driver, cls, events): ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, }, ) await hass.async_block_till_done() @@ -186,7 +186,7 @@ async def test_thermostat(hass, hk_driver, cls, events): ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, }, ) await hass.async_block_till_done() @@ -203,7 +203,7 @@ async def test_thermostat(hass, hk_driver, cls, events): ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -265,7 +265,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, }, ) await hass.async_block_till_done() @@ -284,7 +284,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, }, ) await hass.async_block_till_done() @@ -303,7 +303,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -349,7 +349,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, }, ) await hass.async_block_till_done() @@ -367,7 +367,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -381,7 +381,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): ATTR_HVAC_MODE: HVAC_MODE_OFF, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index c366761ea1f..630174a0661 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -13,7 +13,7 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_FAN_MODES, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_TARGET_TEMP_HIGH, @@ -214,7 +214,7 @@ async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) - assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_AUTO, HVAC_MODE_COOL, @@ -238,7 +238,7 @@ async def test_basic_thermostat_entity_state(hass, basic_thermostat): state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) - assert ATTR_HVAC_ACTIONS not in state.attributes + assert ATTR_HVAC_ACTION not in state.attributes assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -259,7 +259,7 @@ async def test_thermostat_entity_state(hass, thermostat): | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) - assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_AUTO, HVAC_MODE_COOL, From a80baf2e5fe9a7973c0fc52a0755979c4ce9a57c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 07:29:10 -0800 Subject: [PATCH 216/306] Add fan device condition (#28549) --- .../components/fan/device_condition.py | 80 +++++++++++ homeassistant/components/fan/strings.json | 4 + tests/components/fan/test_device_condition.py | 126 ++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 homeassistant/components/fan/device_condition.py create mode 100644 tests/components/fan/test_device_condition.py diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py new file mode 100644 index 00000000000..8b567fcd4c9 --- /dev/null +++ b/homeassistant/components/fan/device_condition.py @@ -0,0 +1,80 @@ +"""Provide the device automations for Fan.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_on", "is_off"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Fan devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_on", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_off", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_on": + state = STATE_ON + else: + state = STATE_OFF + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index d566c76994c..134119f41ff 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -1,5 +1,9 @@ { "device_automation": { + "condtion_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, "trigger_type": { "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py new file mode 100644 index 00000000000..ea87e36b636 --- /dev/null +++ b/tests/components/fan/test_device_condition.py @@ -0,0 +1,126 @@ +"""The tests for Fan device conditions.""" +import pytest + +from homeassistant.components.fan import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a fan.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("fan.entity", STATE_ON) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on - event - test_event1" + + hass.states.async_set("fan.entity", STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off - event - test_event2" From 899306c8ec9e17af1405d4639532929423ba1989 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 07:29:39 -0800 Subject: [PATCH 217/306] Add vacuum device conditions (#28551) --- .../components/vacuum/device_condition.py | 79 ++++++++++ homeassistant/components/vacuum/strings.json | 4 + .../vacuum/test_device_condition.py | 138 ++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 homeassistant/components/vacuum/device_condition.py create mode 100644 tests/components/vacuum/test_device_condition.py diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py new file mode 100644 index 00000000000..6a41fe0490e --- /dev/null +++ b/homeassistant/components/vacuum/device_condition.py @@ -0,0 +1,79 @@ +"""Provide the device automations for Vacuum.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN, STATE_DOCKED, STATE_CLEANING, STATE_RETURNING + +CONDITION_TYPES = {"is_cleaning", "is_docked"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Vacuum devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_cleaning", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_docked", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_docked": + test_states = [STATE_DOCKED] + else: + test_states = [STATE_CLEANING, STATE_RETURNING] + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + state = hass.states.get(config[ATTR_ENTITY_ID]) + return state is not None and state.state in test_states + + return test_is_state diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 2a6fcbbd357..0300242a506 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -1,5 +1,9 @@ { "device_automation": { + "condtion_type": { + "is_docked": "{entity_name} is docked", + "is_cleaning": "{entity_name} is cleaning" + }, "trigger_type": { "cleaning": "{entity_name} started cleaning", "docked": "{entity_name} docked" diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py new file mode 100644 index 00000000000..80e7b72c36f --- /dev/null +++ b/tests/components/vacuum/test_device_condition.py @@ -0,0 +1,138 @@ +"""The tests for Vacuum device conditions.""" +import pytest + +from homeassistant.components.vacuum import ( + DOMAIN, + STATE_CLEANING, + STATE_DOCKED, + STATE_RETURNING, +) +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a vacuum.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_cleaning", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_docked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("vacuum.entity", STATE_DOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "vacuum.entity", + "type": "is_cleaning", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_cleaning - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "vacuum.entity", + "type": "is_docked", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_docked - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_docked - event - test_event2" + + hass.states.async_set("vacuum.entity", STATE_CLEANING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_cleaning - event - test_event1" + + # Returning means it's still cleaning + hass.states.async_set("vacuum.entity", STATE_RETURNING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_cleaning - event - test_event1" From 9d3d35ad793724a39909de4aabc872bd9845d847 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 7 Nov 2019 16:41:33 +0100 Subject: [PATCH 218/306] Add cool mode to HomematicIP climate (#28525) * Add cool mode to HomematicIP climate * Update test * remove preset_party * Fix profile_names check --- .../components/homematicip_cloud/climate.py | 154 ++++++++++---- .../homematicip_cloud/test_climate.py | 192 ++++++++++++++++-- tests/fixtures/homematicip_cloud.json | 6 +- 3 files changed, 293 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 74d647c8c33..a8ea424b207 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -4,13 +4,15 @@ from typing import Awaitable from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup -from homematicip.base.enums import AbsenceType, GroupType +from homematicip.base.enums import AbsenceType from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, + HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, @@ -30,6 +32,9 @@ COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5} _LOGGER = logging.getLogger(__name__) +ATTR_PRESET_END_TIME = "preset_end_time" +PERMANENT_END_TIME = "permanent" + HMIP_AUTOMATIC_CM = "AUTOMATIC" HMIP_MANUAL_CM = "MANUAL" HMIP_ECO_CM = "ECO" @@ -55,15 +60,20 @@ async def async_setup_entry( class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): - """Representation of a HomematicIP heating group.""" + """Representation of a HomematicIP heating group. + + Heat mode is supported for all heating devices incl. their defined profiles. + Boost is available for radiator thermostats only. + Cool mode is only available for floor heating systems, if basically enabled in the hmip app. + """ def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: """Initialize heating group.""" device.modelType = "HmIP-Heating-Group" + super().__init__(hap, device) self._simple_heating = None if device.actualTemperature is None: - self._simple_heating = _get_first_heating_thermostat(device) - super().__init__(hap, device) + self._simple_heating = self._get_first_radiator_thermostat() @property def device_info(self): @@ -105,54 +115,66 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): @property def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ + """Return hvac operation ie.""" + if self._disabled_by_cooling_mode: + return HVAC_MODE_OFF if self._device.boostMode: return HVAC_MODE_HEAT if self._device.controlMode == HMIP_MANUAL_CM: - return HVAC_MODE_HEAT + return HVAC_MODE_HEAT if self._heat_mode_enabled else HVAC_MODE_COOL return HVAC_MODE_AUTO @property def hvac_modes(self): - """Return the list of available hvac operation modes. + """Return the list of available hvac operation modes.""" + if self._disabled_by_cooling_mode: + return [HVAC_MODE_OFF] - Need to be a subset of HVAC_MODES. - """ - return [HVAC_MODE_AUTO, HVAC_MODE_HEAT] + return ( + [HVAC_MODE_AUTO, HVAC_MODE_HEAT] + if self._heat_mode_enabled + else [HVAC_MODE_AUTO, HVAC_MODE_COOL] + ) @property def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp. - - Requires SUPPORT_PRESET_MODE. - """ + """Return the current preset mode.""" if self._device.boostMode: return PRESET_BOOST - if self.hvac_mode == HVAC_MODE_HEAT: + if self.hvac_mode in (HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF): return PRESET_NONE if self._device.controlMode == HMIP_ECO_CM: - absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType - if absence_type == AbsenceType.VACATION: + if self._indoor_climate.absenceType == AbsenceType.VACATION: return PRESET_AWAY - if absence_type in [ + if self._indoor_climate.absenceType in [ + AbsenceType.PARTY, AbsenceType.PERIOD, AbsenceType.PERMANENT, - AbsenceType.PARTY, ]: return PRESET_ECO - if self._device.activeProfile: - return self._device.activeProfile.name + return ( + self._device.activeProfile.name + if self._device.activeProfile.name in self._device_profile_names + else None + ) @property def preset_modes(self): - """Return a list of available preset modes incl profiles.""" - presets = [PRESET_NONE, PRESET_BOOST] - presets.extend(self._device_profile_names) + """Return a list of available preset modes incl. hmip profiles.""" + # Boost is only available if a radiator thermostat is in the room, + # and heat mode is enabled. + profile_names = self._device_profile_names + + presets = [] + if self._heat_mode_enabled and self._has_radiator_thermostat: + if not profile_names: + presets.append(PRESET_NONE) + presets.append(PRESET_BOOST) + + presets.extend(profile_names) + return presets @property @@ -170,10 +192,15 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - await self._device.set_point_temperature(temperature) + + if self.min_temp <= temperature <= self.max_temp: + await self._device.set_point_temperature(temperature) async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: """Set new target hvac mode.""" + if hvac_mode not in self.hvac_modes: + return + if hvac_mode == HVAC_MODE_AUTO: await self._device.set_control_mode(HMIP_AUTOMATIC_CM) else: @@ -181,18 +208,44 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: """Set new preset mode.""" + if preset_mode not in self.preset_modes: + return + if self._device.boostMode and preset_mode != PRESET_BOOST: await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() if preset_mode in self._device_profile_names: profile_idx = self._get_profile_idx_by_name(preset_mode) - await self.async_set_hvac_mode(HVAC_MODE_AUTO) + if self._device.controlMode != HMIP_AUTOMATIC_CM: + await self.async_set_hvac_mode(HVAC_MODE_AUTO) await self._device.set_active_profile(profile_idx) + @property + def device_state_attributes(self): + """Return the state attributes of the access point.""" + state_attr = super().device_state_attributes + + if self._device.controlMode == HMIP_ECO_CM: + if self._indoor_climate.absenceType in [ + AbsenceType.PARTY, + AbsenceType.PERIOD, + AbsenceType.VACATION, + ]: + state_attr[ATTR_PRESET_END_TIME] = self._indoor_climate.absenceEndTime + elif self._indoor_climate.absenceType == AbsenceType.PERMANENT: + state_attr[ATTR_PRESET_END_TIME] = PERMANENT_END_TIME + + return state_attr + + @property + def _indoor_climate(self): + """Return the hmip indoor climate functional home of this group.""" + return self._home.get_functionalHome(IndoorClimateHome) + @property def _device_profiles(self): - """Return the relevant profiles of the device.""" + """Return the relevant profiles.""" return [ profile for profile in self._device.profiles @@ -218,17 +271,36 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): return relevant_index[index_name[0]] @property - def _relevant_profile_group(self): - """Return the relevant profile groups.""" - return ( - HEATING_PROFILES - if self._device.groupType == GroupType.HEATING - else COOLING_PROFILES + def _heat_mode_enabled(self): + """Return, if heating mode is enabled.""" + return not self._device.cooling + + @property + def _disabled_by_cooling_mode(self): + """Return, if group is disabled by the cooling mode.""" + return self._device.cooling and ( + self._device.coolingIgnored or not self._device.coolingAllowed ) + @property + def _relevant_profile_group(self): + """Return the relevant profile groups.""" + if self._disabled_by_cooling_mode: + return [] -def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): - """Return the first HeatingThermostat from a HeatingGroup.""" - for device in heating_group.devices: - if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): - return device + return HEATING_PROFILES if self._heat_mode_enabled else COOLING_PROFILES + + @property + def _has_radiator_thermostat(self) -> bool: + """Return, if a radiator thermostat is in the hmip heating group.""" + return bool(self._get_first_radiator_thermostat()) + + def _get_first_radiator_thermostat(self): + """Return the first radiator thermostat from the hmip heating group.""" + for device in self._device.devices: + if isinstance( + device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact) + ): + return device + + return None diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 80e4e74e451..6a05a880864 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -10,13 +10,18 @@ from homeassistant.components.climate.const import ( ATTR_PRESET_MODE, ATTR_PRESET_MODES, HVAC_MODE_AUTO, + HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, - PRESET_NONE, ) from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.climate import ( + ATTR_PRESET_END_TIME, + PERMANENT_END_TIME, +) from homeassistant.setup import async_setup_component from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics @@ -33,7 +38,7 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_heating_group(hass, default_mock_hap): +async def test_hmip_heating_group_heat(hass, default_mock_hap): """Test HomematicipHeatingGroup.""" entity_id = "climate.badezimmer" entity_name = "Badezimmer" @@ -50,12 +55,7 @@ async def test_hmip_heating_group(hass, default_mock_hap): assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 47 assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" - assert ha_state.attributes[ATTR_PRESET_MODES] == [ - PRESET_NONE, - PRESET_BOOST, - "STD", - "Winter", - ] + assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "Winter"] service_call_counter = len(hmip_device.mock_calls) @@ -114,12 +114,12 @@ async def test_hmip_heating_group(hass, default_mock_hap): await hass.services.async_call( "climate", "set_preset_mode", - {"entity_id": entity_id, "preset_mode": PRESET_NONE}, + {"entity_id": entity_id, "preset_mode": "STD"}, blocking=True, ) - assert len(hmip_device.mock_calls) == service_call_counter + 9 - assert hmip_device.mock_calls[-1][0] == "set_boost" - assert hmip_device.mock_calls[-1][1] == (False,) + assert len(hmip_device.mock_calls) == service_call_counter + 11 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (0,) await async_manipulate_test_data(hass, hmip_device, "boostMode", False) ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" @@ -132,7 +132,7 @@ async def test_hmip_heating_group(hass, default_mock_hap): blocking=True, ) # No new service call should be in mock_calls. - assert len(hmip_device.mock_calls) == service_call_counter + 10 + assert len(hmip_device.mock_calls) == service_call_counter + 12 # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" @@ -158,7 +158,6 @@ async def test_hmip_heating_group(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO - # Not required for hmip, but a posiblity to send no temperature. await hass.services.async_call( "climate", "set_preset_mode", @@ -166,10 +165,173 @@ async def test_hmip_heating_group(hass, default_mock_hap): blocking=True, ) - assert len(hmip_device.mock_calls) == service_call_counter + 16 + assert len(hmip_device.mock_calls) == service_call_counter + 18 assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) + default_mock_hap.home.get_functionalHome( + IndoorClimateHome + ).absenceType = AbsenceType.PERMANENT + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_END_TIME] == PERMANENT_END_TIME + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 20 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("MANUAL",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_HEAT + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Winter"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 23 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + hmip_device.activeProfile = hmip_device.profiles[0] + await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTOMATIC") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": "dry"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 24 + # Only fire event from last async_manipulate_test_data available. + assert hmip_device.mock_calls[-1][0] == "fire_update_event" + + +async def test_hmip_heating_group_cool(hass, default_mock_hap): + """Test HomematicipHeatingGroup.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + hmip_device.activeProfile = hmip_device.profiles[3] + await async_manipulate_test_data(hass, hmip_device, "cooling", True) + await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True) + await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes["current_temperature"] == 23.8 + assert ha_state.attributes["min_temp"] == 5.0 + assert ha_state.attributes["max_temp"] == 30.0 + assert ha_state.attributes["temperature"] == 5.0 + assert ha_state.attributes["current_humidity"] == 47 + assert ha_state.attributes[ATTR_PRESET_MODE] == "Cool1" + assert ha_state.attributes[ATTR_PRESET_MODES] == ["Cool1", "Cool2"] + + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_COOL}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("MANUAL",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_COOL + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_AUTO}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("AUTOMATIC",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTO") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Cool2"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 6 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (4,) + + hmip_device.activeProfile = hmip_device.profiles[4] + await async_manipulate_test_data(hass, hmip_device, "cooling", True) + await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", False) + await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == HVAC_MODE_OFF + assert ha_state.attributes[ATTR_PRESET_MODE] == "none" + assert ha_state.attributes[ATTR_PRESET_MODES] == [] + + hmip_device.activeProfile = hmip_device.profiles[4] + await async_manipulate_test_data(hass, hmip_device, "cooling", True) + await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True) + await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", True) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == HVAC_MODE_OFF + assert ha_state.attributes[ATTR_PRESET_MODE] == "none" + assert ha_state.attributes[ATTR_PRESET_MODES] == [] + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Cool2"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 12 + # fire_update_event shows that set_active_profile has not been called. + assert hmip_device.mock_calls[-1][0] == "fire_update_event" + + hmip_device.activeProfile = hmip_device.profiles[4] + await async_manipulate_test_data(hass, hmip_device, "cooling", True) + await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True) + await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes[ATTR_PRESET_MODE] == "Cool2" + assert ha_state.attributes[ATTR_PRESET_MODES] == ["Cool1", "Cool2"] + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Cool2"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 17 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (4,) + async def test_hmip_climate_services(hass, mock_hap_with_service): """Test HomematicipHeatingGroup.""" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index e17df9c2039..d1ef82f4234 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -4662,7 +4662,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_4", - "name": "", + "name": "Cool1", "profileId": "00000000-0000-0000-0000-000000000041", "visible": true }, @@ -4670,9 +4670,9 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_5", - "name": "", + "name": "Cool2", "profileId": "00000000-0000-0000-0000-000000000042", - "visible": false + "visible": true }, "PROFILE_6": { "enabled": true, From 9b5fa2e67cf5208e9618a79b3e5b5559d1f495fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 08:03:06 -0800 Subject: [PATCH 219/306] Add device conditions to climate (#28553) * Add device conditions to climate * Update strings.json --- .../components/climate/device_condition.py | 117 +++++++++ homeassistant/components/climate/strings.json | 4 + .../climate/test_device_condition.py | 234 ++++++++++++++++++ 3 files changed, 355 insertions(+) create mode 100644 homeassistant/components/climate/device_condition.py create mode 100644 tests/components/climate/test_device_condition.py diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py new file mode 100644 index 00000000000..c923f3123f1 --- /dev/null +++ b/homeassistant/components/climate/device_condition.py @@ -0,0 +1,117 @@ +"""Provide the device automations for Climate.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN, const + +CONDITION_TYPES = {"is_hvac_mode", "is_preset_mode"} + +HVAC_MODE_CONDITION = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): "is_hvac_mode", + vol.Required(const.ATTR_HVAC_MODE): vol.In(const.HVAC_MODES), + } +) + +PRESET_MODE_CONDITION = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): "is_preset_mode", + vol.Required(const.ATTR_PRESET_MODE): str, + } +) + +CONDITION_SCHEMA = vol.Any(HVAC_MODE_CONDITION, PRESET_MODE_CONDITION) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Climate devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_hvac_mode", + } + ) + + if state and const.ATTR_PRESET_MODES in state.attributes: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_preset_mode", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + + if config[CONF_TYPE] == "is_hvac_mode": + attribute = const.ATTR_HVAC_MODE + else: + attribute = const.ATTR_PRESET_MODE + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + state = hass.states.get(config[ATTR_ENTITY_ID]) + return state and state.attributes.get(attribute) == config[attribute] + + return test_is_state + + +async def async_get_condition_capabilities(hass, config): + """List condition capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + condition_type = config[CONF_TYPE] + + fields = {} + + if condition_type == "is_hvac_mode": + hvac_modes = state.attributes[const.ATTR_HVAC_MODES] if state else [] + fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes) + + elif condition_type == "is_preset_mode": + if state: + preset_modes = state.attributes.get(const.ATTR_PRESET_MODES, []) + else: + preset_modes = [] + + fields[vol.Required(const.ATTR_PRESET_MODES)] = vol.In(preset_modes) + + return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 84854c713cf..a2ceeff2143 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -1,5 +1,9 @@ { "device_automation": { + "condtion_type": { + "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", + "is_preset_mode": "{entity_name} is set to a specific preset mode" + }, "trigger_type": { "current_temperature_changed": "{entity_name} measured temperature changed", "current_humidity_changed": "{entity_name} measured humidity changed", diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py new file mode 100644 index 00000000000..82b6f595fb0 --- /dev/null +++ b/tests/components/climate/test_device_condition.py @@ -0,0 +1,234 @@ +"""The tests for Climate device conditions.""" +import pytest +import voluptuous_serialize + +from homeassistant.components.climate import DOMAIN, const, device_condition +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry, config_validation as cv + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a climate.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set( + f"{DOMAIN}.test_5678", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_hvac_mode", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_preset_mode", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "is_hvac_mode", + "hvac_mode": "cool", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_hvac_mode - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "is_preset_mode", + "preset_mode": "away", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_preset_mode - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_hvac_mode - event - test_event1" + + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_AUTO, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + }, + ) + + # Should not fire + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + + assert len(calls) == 2 + assert calls[1].data["some"] == "is_preset_mode - event - test_event2" + + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_AUTO, + const.ATTR_PRESET_MODE: const.PRESET_HOME, + }, + ) + + # Should not fire + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + + +async def test_capabilities(hass): + """Bla.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + + # Test hvac mode + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "is_hvac_mode", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ] + + # Test preset mode + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "is_preset_mode", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "preset_modes", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ] From fadb6a39790c688501d8415d94e0047aca40057b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 12:21:12 -0800 Subject: [PATCH 220/306] Add support for conversation ID (#28620) --- homeassistant/components/almond/__init__.py | 7 +++++-- homeassistant/components/conversation/__init__.py | 14 +++++++++----- homeassistant/components/conversation/agent.py | 5 ++++- .../components/conversation/default_agent.py | 5 ++++- tests/components/conversation/test_init.py | 14 ++++++++++++-- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 115e0d24de4..5eb305e6795 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import logging import time +from typing import Optional import async_timeout from aiohttp import ClientSession, ClientError @@ -205,9 +206,11 @@ class AlmondAgent(conversation.AbstractConversationAgent): """Initialize the agent.""" self.api = api - async def async_process(self, text: str) -> intent.IntentResponse: + async def async_process( + self, text: str, conversation_id: Optional[str] = None + ) -> intent.IntentResponse: """Process a sentence.""" - response = await self.api.async_converse_text(text) + response = await self.api.async_converse_text(text, conversation_id) buffer = "" for message in response["messages"]: diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 798fc926e0f..f875ec2822c 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -53,7 +53,7 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): async def async_setup(hass, config): """Register the process service.""" - async def process(hass, text): + async def process(hass, text, conversation_id): """Process a line of text.""" agent = hass.data.get(DATA_AGENT) @@ -61,14 +61,14 @@ async def async_setup(hass, config): agent = hass.data[DATA_AGENT] = DefaultAgent(hass) await agent.async_initialize(config) - return await agent.async_process(text) + return await agent.async_process(text, conversation_id) async def handle_service(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) try: - await process(hass, text) + await process(hass, text, service.context.id) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) @@ -91,13 +91,17 @@ class ConversationProcessView(http.HomeAssistantView): """Initialize the conversation process view.""" self._process = process - @RequestDataValidator(vol.Schema({vol.Required("text"): str})) + @RequestDataValidator( + vol.Schema({vol.Required("text"): str, vol.Optional("conversation_id"): str}) + ) async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] try: - intent_result = await self._process(hass, data["text"]) + intent_result = await self._process( + hass, data["text"], data.get("conversation_id") + ) except intent.IntentHandleError as err: intent_result = intent.IntentResponse() intent_result.async_set_speech(str(err)) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index eae6402530c..1875ab5b9b9 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -1,5 +1,6 @@ """Agent foundation for conversation integration.""" from abc import ABC, abstractmethod +from typing import Optional from homeassistant.helpers import intent @@ -8,5 +9,7 @@ class AbstractConversationAgent(ABC): """Abstract conversation agent.""" @abstractmethod - async def async_process(self, text: str) -> intent.IntentResponse: + async def async_process( + self, text: str, conversation_id: Optional[str] = None + ) -> intent.IntentResponse: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index e93afcfaf65..c202cdf1e65 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1,6 +1,7 @@ """Standard conversastion implementation for Home Assistant.""" import logging import re +from typing import Optional from homeassistant import core from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER @@ -107,7 +108,9 @@ class DefaultAgent(AbstractConversationAgent): for intent_type, sentences in UTTERANCES[component].items(): async_register(self.hass, intent_type, sentences) - async def async_process(self, text) -> intent.IntentResponse: + async def async_process( + self, text: str, conversation_id: Optional[str] = None + ) -> intent.IntentResponse: """Process a sentence.""" intents = self.hass.data[DOMAIN] diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index a9116ac0d98..ff44eaccc8e 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -266,11 +266,14 @@ async def test_http_api_wrong_data(hass, hass_client): async def test_custom_agent(hass, hass_client): """Test a custom conversation agent.""" + calls = [] + class MyAgent(conversation.AbstractConversationAgent): """Test Agent.""" - async def async_process(self, text): + async def async_process(self, text, conversation_id): """Process some text.""" + calls.append((text, conversation_id)) response = intent.IntentResponse() response.async_set_speech("Test response") return response @@ -281,9 +284,16 @@ async def test_custom_agent(hass, hass_client): client = await hass_client() - resp = await client.post("/api/conversation/process", json={"text": "Test Text"}) + resp = await client.post( + "/api/conversation/process", + json={"text": "Test Text", "conversation_id": "test-conv-id"}, + ) assert resp.status == 200 assert await resp.json() == { "card": {}, "speech": {"plain": {"extra_data": None, "speech": "Test response"}}, } + + assert len(calls) == 1 + assert calls[0][0] == "Test Text" + assert calls[0][1] == "test-conv-id" From 2bdfa9928bce20ecf238c03944f7fee1eff3c26a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 7 Nov 2019 14:54:48 -0600 Subject: [PATCH 221/306] Allow to skip SSL validation on Plex websocket (#28615) --- homeassistant/components/plex/__init__.py | 5 ++++- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 1aaa8a8e3aa..4a575722826 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -160,7 +160,10 @@ async def async_setup_entry(hass, entry): async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) session = async_get_clientsession(hass) - websocket = PlexWebsocket(plex_server.plex_server, update_plex, session) + verify_ssl = server_config.get(CONF_VERIFY_SSL) + websocket = PlexWebsocket( + plex_server.plex_server, update_plex, session=session, verify_ssl=verify_ssl + ) hass.loop.create_task(websocket.listen()) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 8edccda75e0..b7179174ea4 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.0.6", "plexauth==0.0.5", - "plexwebsocket==0.0.3" + "plexwebsocket==0.0.4" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index 8843a6693ab..2efb1db7ba0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -983,7 +983,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.3 +plexwebsocket==0.0.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 186d2576f72..f29eee878d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.3 +plexwebsocket==0.0.4 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From 10122157093d9c5787d2eb8200c76573b67874e4 Mon Sep 17 00:00:00 2001 From: "Brett T. Warden" Date: Thu, 7 Nov 2019 14:03:32 -0800 Subject: [PATCH 222/306] Match ALARM in NUT UPS status message (#28591) If ups.status contains "ALARM", add "Alarm" to virtual sensor ups.status.display. Fixes #28580 --- homeassistant/components/nut/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index c7865f8c0f1..db485734777 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -133,6 +133,7 @@ STATE_TYPES = { "TRIM": "Trimming Voltage", "BOOST": "Boosting Voltage", "FSD": "Forced Shutdown", + "ALARM": "Alarm", } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( From a71d852f16b94d5ac892a5c2e31bcad9140bd410 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 7 Nov 2019 14:04:59 -0800 Subject: [PATCH 223/306] Use friendly app names for Fire TV sources (#28417) * Use friendly app names for Fire TV sources * Remove debugging statement * Tests pass * Use 'blocking=True' to patch service calls * Remove parentheses --- .../components/androidtv/media_player.py | 31 +++-- tests/components/androidtv/patchers.py | 12 ++ .../components/androidtv/test_media_player.py | 114 +++++++++++++++++- 3 files changed, 145 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 60f423c3dee..7540973ea19 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -287,8 +287,11 @@ class ADBDevice(MediaPlayerDevice): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv self._name = name - self._apps = APPS.copy() - self._apps.update(apps) + self._app_id_to_name = APPS.copy() + self._app_id_to_name.update(apps) + self._app_name_to_id = { + value: key for key, value in self._app_id_to_name.items() + } self._keys = KEYS self._device_properties = self.aftv.device_properties @@ -328,7 +331,7 @@ class ADBDevice(MediaPlayerDevice): @property def app_name(self): """Return the friendly name of the current app.""" - return self._apps.get(self._current_app, self._current_app) + return self._app_id_to_name.get(self._current_app, self._current_app) @property def available(self): @@ -518,7 +521,7 @@ class FireTVDevice(ADBDevice): super().__init__(aftv, name, apps, turn_on_command, turn_off_command) self._get_sources = get_sources - self._running_apps = None + self._sources = None @adb_decorator(override_available=True) def update(self): @@ -538,23 +541,28 @@ class FireTVDevice(ADBDevice): return # Get the `state`, `current_app`, and `running_apps`. - state, self._current_app, self._running_apps = self.aftv.update( - self._get_sources - ) + state, self._current_app, running_apps = self.aftv.update(self._get_sources) self._state = ANDROIDTV_STATES.get(state) if self._state is None: self._available = False + if running_apps: + self._sources = [ + self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + ] + else: + self._sources = None + @property def source(self): """Return the current app.""" - return self._current_app + return self._app_id_to_name.get(self._current_app, self._current_app) @property def source_list(self): """Return a list of running apps.""" - return self._running_apps + return self._sources @property def supported_features(self): @@ -575,6 +583,7 @@ class FireTVDevice(ADBDevice): """ if isinstance(source, str): if not source.startswith("!"): - self.aftv.launch_app(source) + self.aftv.launch_app(self._app_name_to_id.get(source, source)) else: - self.aftv.stop_app(source[1:].lstrip()) + source_ = source[1:].lstrip() + self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 5fc6bc754fa..986180bf214 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -140,3 +140,15 @@ def isfile(filepath): PATCH_ISFILE = patch("os.path.isfile", isfile) PATCH_ACCESS = patch("os.access", return_value=True) + + +def patch_firetv_update(state, current_app, running_apps): + """Patch the `FireTV.update()` method.""" + return patch( + "androidtv.firetv.FireTV.update", + return_value=(state, current_app, running_apps), + ) + + +PATCH_LAUNCH_APP = patch("androidtv.firetv.FireTV.launch_app") +PATCH_STOP_APP = patch("androidtv.firetv.FireTV.stop_app") diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 85f562a3500..04b0bebf447 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -6,15 +6,22 @@ from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, CONF_ADBKEY, + CONF_APPS, +) +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + DOMAIN, + SERVICE_SELECT_SOURCE, ) -from homeassistant.components.media_player.const import DOMAIN from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PLATFORM, STATE_IDLE, STATE_OFF, + STATE_PLAYING, STATE_UNAVAILABLE, ) @@ -276,3 +283,108 @@ async def test_setup_with_adbkey(hass): state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF + + +async def test_firetv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Fire TV devices.""" + config = CONFIG_FIRETV_ADB_SERVER.copy() + config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + with patchers.patch_firetv_update( + "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_PLAYING + assert state.attributes["source"] == "TEST 1" + assert state.attributes["source_list"] == ["TEST 1", "com.app.test2"] + + with patchers.patch_firetv_update( + "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_PLAYING + assert state.attributes["source"] == "com.app.test2" + assert state.attributes["source_list"] == ["com.app.test2", "TEST 1"] + + +async def _test_firetv_select_source(hass, source, expected_arg, method_patch): + """Test that the `FireTV.launch_app` and `FireTV.stop_app` methods are called with the right parameter.""" + config = CONFIG_FIRETV_ADB_SERVER.copy() + config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + with method_patch as method_patch_: + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: entity_id, ATTR_INPUT_SOURCE: source}, + blocking=True, + ) + method_patch_.assert_called_with(expected_arg) + + return True + + +async def test_firetv_select_source_launch_app_id(hass): + """Test that an app can be launched using its app ID.""" + assert await _test_firetv_select_source( + hass, "com.app.test1", "com.app.test1", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_launch_app_name(hass): + """Test that an app can be launched using its friendly name.""" + assert await _test_firetv_select_source( + hass, "TEST 1", "com.app.test1", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_launch_app_id_no_name(hass): + """Test that an app can be launched using its app ID when it has no friendly name.""" + assert await _test_firetv_select_source( + hass, "com.app.test2", "com.app.test2", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_stop_app_id(hass): + """Test that an app can be stopped using its app ID.""" + assert await _test_firetv_select_source( + hass, "!com.app.test1", "com.app.test1", patchers.PATCH_STOP_APP + ) + + +async def test_firetv_select_source_stop_app_name(hass): + """Test that an app can be stopped using its friendly name.""" + assert await _test_firetv_select_source( + hass, "!TEST 1", "com.app.test1", patchers.PATCH_STOP_APP + ) + + +async def test_firetv_select_source_stop_app_id_no_name(hass): + """Test that an app can be stopped using its app ID when it has no friendly name.""" + assert await _test_firetv_select_source( + hass, "!com.app.test2", "com.app.test2", patchers.PATCH_STOP_APP + ) From 64166583b3ac75298925dde81beecc1e3444a322 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 8 Nov 2019 00:32:12 +0000 Subject: [PATCH 224/306] [ci skip] Translation update --- .../almond/.translations/zh-Hant.json | 3 +- .../components/climate/.translations/en.json | 17 ++++++++++ .../components/climate/.translations/it.json | 17 ++++++++++ .../climate/.translations/zh-Hant.json | 8 +++++ .../coolmaster/.translations/de.json | 11 ++++++ .../components/deconz/.translations/fr.json | 7 ++++ .../components/deconz/.translations/it.json | 14 +++++++- .../device_tracker/.translations/de.json | 8 +++++ .../components/fan/.translations/de.json | 16 +++++++++ .../components/fan/.translations/en.json | 16 +++++++++ .../components/fan/.translations/it.json | 16 +++++++++ .../components/fan/.translations/ru.json | 12 +++++++ .../components/fan/.translations/zh-Hant.json | 12 +++++++ .../huawei_lte/.translations/de.json | 34 +++++++++++++++++++ .../huawei_lte/.translations/it.json | 2 +- .../components/lock/.translations/de.json | 9 +++++ .../components/lock/.translations/en.json | 4 +++ .../components/lock/.translations/it.json | 4 +++ .../components/lock/.translations/ru.json | 4 +++ .../lock/.translations/zh-Hant.json | 4 +++ .../media_player/.translations/de.json | 9 +++++ .../components/sensor/.translations/de.json | 2 +- .../components/somfy/.translations/de.json | 5 +++ .../transmission/.translations/de.json | 2 ++ .../components/vacuum/.translations/de.json | 7 ++++ .../components/vacuum/.translations/en.json | 16 +++++++++ .../components/vacuum/.translations/it.json | 16 +++++++++ .../components/vacuum/.translations/ru.json | 8 +++++ .../vacuum/.translations/zh-Hant.json | 12 +++++++ .../components/withings/.translations/de.json | 6 ++++ .../components/wled/.translations/ca.json | 17 ++++++++++ .../components/wled/.translations/de.json | 6 ++++ .../components/wled/.translations/it.json | 25 ++++++++++++++ .../components/wled/.translations/ru.json | 26 ++++++++++++++ .../wled/.translations/zh-Hant.json | 26 ++++++++++++++ 35 files changed, 397 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/climate/.translations/en.json create mode 100644 homeassistant/components/climate/.translations/it.json create mode 100644 homeassistant/components/climate/.translations/zh-Hant.json create mode 100644 homeassistant/components/coolmaster/.translations/de.json create mode 100644 homeassistant/components/device_tracker/.translations/de.json create mode 100644 homeassistant/components/fan/.translations/de.json create mode 100644 homeassistant/components/fan/.translations/en.json create mode 100644 homeassistant/components/fan/.translations/it.json create mode 100644 homeassistant/components/fan/.translations/ru.json create mode 100644 homeassistant/components/fan/.translations/zh-Hant.json create mode 100644 homeassistant/components/huawei_lte/.translations/de.json create mode 100644 homeassistant/components/media_player/.translations/de.json create mode 100644 homeassistant/components/vacuum/.translations/de.json create mode 100644 homeassistant/components/vacuum/.translations/en.json create mode 100644 homeassistant/components/vacuum/.translations/it.json create mode 100644 homeassistant/components/vacuum/.translations/ru.json create mode 100644 homeassistant/components/vacuum/.translations/zh-Hant.json create mode 100644 homeassistant/components/wled/.translations/ca.json create mode 100644 homeassistant/components/wled/.translations/de.json create mode 100644 homeassistant/components/wled/.translations/it.json create mode 100644 homeassistant/components/wled/.translations/ru.json create mode 100644 homeassistant/components/wled/.translations/zh-Hant.json diff --git a/homeassistant/components/almond/.translations/zh-Hant.json b/homeassistant/components/almond/.translations/zh-Hant.json index c84b2dd432b..743835b1046 100644 --- a/homeassistant/components/almond/.translations/zh-Hant.json +++ b/homeassistant/components/almond/.translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Almond \u5e33\u865f\u3002", - "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002" + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002", + "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002" }, "title": "Almond" } diff --git a/homeassistant/components/climate/.translations/en.json b/homeassistant/components/climate/.translations/en.json new file mode 100644 index 00000000000..942d9a2761f --- /dev/null +++ b/homeassistant/components/climate/.translations/en.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Change HVAC mode on {entity_name}", + "set_preset_mode": "Change preset on {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", + "is_preset_mode": "{entity_name} is set to a specific preset mode" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} measured humidity changed", + "current_temperature_changed": "{entity_name} measured temperature changed", + "hvac_mode_changed": "{entity_name} HVAC mode changed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/it.json b/homeassistant/components/climate/.translations/it.json new file mode 100644 index 00000000000..34ecbf5e9f2 --- /dev/null +++ b/homeassistant/components/climate/.translations/it.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Cambia modalit\u00e0 HVAC su {entity_name}", + "set_preset_mode": "Modifica preimpostazione su {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 HVAC specifica", + "is_preset_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 preimpostata specifica" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} umidit\u00e0 misurata modificata", + "current_temperature_changed": "{entity_name} temperatura misurata cambiata", + "hvac_mode_changed": "{entity_name} modalit\u00e0 HVAC modificata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/zh-Hant.json b/homeassistant/components/climate/.translations/zh-Hant.json new file mode 100644 index 00000000000..a1d603c5552 --- /dev/null +++ b/homeassistant/components/climate/.translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u8b8a\u66f4 {entity_name} HVAC \u6a21\u5f0f", + "set_preset_mode": "\u8b8a\u66f4 {entity_name} \u8a2d\u5b9a\u6a21\u5f0f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/de.json b/homeassistant/components/coolmaster/.translations/de.json new file mode 100644 index 00000000000..66c6911cf10 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "off": "Kann ausgeschaltet werden" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 3b29dbf486d..4d49bd18d1e 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -55,10 +55,17 @@ "left": "Gauche", "open": "Ouvert", "right": "Droite", + "side_1": "Face 1", + "side_2": "Face 2", + "side_3": "Face 3", + "side_4": "Face 4", + "side_5": "Face 5", + "side_6": "Face 6", "turn_off": "\u00c9teint", "turn_on": "Allum\u00e9" }, "trigger_type": { + "remote_awakened": "Appareil r\u00e9veill\u00e9", "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", "remote_button_long_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9 apr\u00e8s appui long", diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 975d69a450f..e2e0d529064 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -55,10 +55,17 @@ "left": "Sinistra", "open": "Aperto", "right": "Destra", + "side_1": "Lato 1", + "side_2": "Lato 2", + "side_3": "Lato 3", + "side_4": "Lato 4", + "side_5": "Lato 5", + "side_6": "Lato 6", "turn_off": "Spegnere", "turn_on": "Accendere" }, "trigger_type": { + "remote_awakened": "Dispositivo risvegliato", "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", @@ -69,7 +76,12 @@ "remote_button_short_press": "Pulsante \"{subtype}\" premuto", "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", - "remote_gyro_activated": "Dispositivo in vibrazione" + "remote_double_tap": "Dispositivo \"{subtype}\" toccato due volte", + "remote_falling": "Dispositivo in caduta libera", + "remote_gyro_activated": "Dispositivo in vibrazione", + "remote_moved": "Dispositivo spostato con \"{subtype}\" verso l'alto", + "remote_rotate_from_side_1": "Dispositivo ruotato da \"lato 1\" a \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/de.json b/homeassistant/components/device_tracker/.translations/de.json new file mode 100644 index 00000000000..7e72bd5595a --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} ist Zuhause", + "is_not_home": "{entity_name} ist nicht zu Hause" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/de.json b/homeassistant/components/fan/.translations/de.json new file mode 100644 index 00000000000..9ac3d999370 --- /dev/null +++ b/homeassistant/components/fan/.translations/de.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condtion_type": { + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, + "trigger_type": { + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/en.json b/homeassistant/components/fan/.translations/en.json new file mode 100644 index 00000000000..b085e7baa45 --- /dev/null +++ b/homeassistant/components/fan/.translations/en.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Turn off {entity_name}", + "turn_on": "Turn on {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on" + }, + "trigger_type": { + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/it.json b/homeassistant/components/fan/.translations/it.json new file mode 100644 index 00000000000..b62d80c793b --- /dev/null +++ b/homeassistant/components/fan/.translations/it.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Spegnere {entity_name}", + "turn_on": "Accendere {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso" + }, + "trigger_type": { + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ru.json b/homeassistant/components/fan/.translations/ru.json new file mode 100644 index 00000000000..abbecabd13f --- /dev/null +++ b/homeassistant/components/fan/.translations/ru.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/zh-Hant.json b/homeassistant/components/fan/.translations/zh-Hant.json new file mode 100644 index 00000000000..58496f9e9bc --- /dev/null +++ b/homeassistant/components/fan/.translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u95dc\u9589 {entity_name}", + "turn_on": "\u958b\u555f {entity_name}" + }, + "trigger_type": { + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/de.json b/homeassistant/components/huawei_lte/.translations/de.json new file mode 100644 index 00000000000..c3f4025b8b6 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert", + "already_in_progress": "Dieses Ger\u00e4t wurde bereits konfiguriert" + }, + "error": { + "connection_failed": "Verbindung fehlgeschlagen.", + "connection_timeout": "Verbindungszeit\u00fcberschreitung", + "incorrect_password": "Ung\u00fcltiges Passwort", + "incorrect_username": "Ung\u00fcltiger Benutzername", + "incorrect_username_or_password": "Ung\u00fcltiger Benutzername oder Kennwort", + "invalid_url": "Ung\u00fcltige URL" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "url": "URL", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "SMS-Benachrichtigungsempf\u00e4nger" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index 0be2ed23462..0646cd4da52 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Questo dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato configurato" }, "error": { "connection_failed": "Connessione fallita", diff --git a/homeassistant/components/lock/.translations/de.json b/homeassistant/components/lock/.translations/de.json index 02c387ff487..443c70b68dd 100644 --- a/homeassistant/components/lock/.translations/de.json +++ b/homeassistant/components/lock/.translations/de.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "lock": "Sperre {entity_name}", + "open": "\u00d6ffne {entity_name}", + "unlock": "Entsperre {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} ist gesperrt", "is_unlocked": "{entity_name} ist entsperrt" + }, + "trigger_type": { + "locked": "{entity_name} gesperrt", + "unlocked": "{entity_name} entsperrt" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/en.json b/homeassistant/components/lock/.translations/en.json index a9800eecadd..262ba27d951 100644 --- a/homeassistant/components/lock/.translations/en.json +++ b/homeassistant/components/lock/.translations/en.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked" + }, + "trigger_type": { + "locked": "{entity_name} locked", + "unlocked": "{entity_name} unlocked" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/it.json b/homeassistant/components/lock/.translations/it.json index 05f0db78cdf..72c350dad24 100644 --- a/homeassistant/components/lock/.translations/it.json +++ b/homeassistant/components/lock/.translations/it.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u00e8 bloccato", "is_unlocked": "{entity_name} \u00e8 sbloccato" + }, + "trigger_type": { + "locked": "{entity_name} \u00e8 bloccato", + "unlocked": "{entity_name} \u00e8 sbloccato" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ru.json b/homeassistant/components/lock/.translations/ru.json index 1610668721f..479fa4bee21 100644 --- a/homeassistant/components/lock/.translations/ru.json +++ b/homeassistant/components/lock/.translations/ru.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_unlocked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + }, + "trigger_type": { + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "unlocked": "{entity_name} \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json index 7c8abb76e16..b5d69a21f9a 100644 --- a/homeassistant/components/lock/.translations/zh-Hant.json +++ b/homeassistant/components/lock/.translations/zh-Hant.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u5df2\u4e0a\u9396", "is_unlocked": "{entity_name} \u5df2\u89e3\u9396" + }, + "trigger_type": { + "locked": "{entity_name} \u5df2\u4e0a\u9396", + "unlocked": "{entity_name} \u5df2\u89e3\u9396" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/de.json b/homeassistant/components/media_player/.translations/de.json new file mode 100644 index 00000000000..42fdc22f377 --- /dev/null +++ b/homeassistant/components/media_player/.translations/de.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "is_paused": "{entity_name} ist pausiert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json index bf28653c0ce..7d5a322ba18 100644 --- a/homeassistant/components/sensor/.translations/de.json +++ b/homeassistant/components/sensor/.translations/de.json @@ -8,7 +8,7 @@ "is_pressure": "{entity_name} Druck", "is_signal_strength": "{entity_name} Signalst\u00e4rke", "is_temperature": "{entity_name} Temperatur", - "is_timestamp": "{entity_name} Zeitstempel", + "is_timestamp": "Aktueller Zeitstempel von {entity_name}", "is_value": "{entity_name} Wert" }, "trigger_type": { diff --git a/homeassistant/components/somfy/.translations/de.json b/homeassistant/components/somfy/.translations/de.json index 1dd1b7b4448..b950a439ef3 100644 --- a/homeassistant/components/somfy/.translations/de.json +++ b/homeassistant/components/somfy/.translations/de.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Erfolgreich mit Somfy authentifiziert." }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json index 1a2fa4a48c0..1847c7186db 100644 --- a/homeassistant/components/transmission/.translations/de.json +++ b/homeassistant/components/transmission/.translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Host ist bereits konfiguriert.", "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, "error": { "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "name_exists": "Name existiert bereits", "wrong_credentials": "Falscher Benutzername oder Kennwort" }, "step": { diff --git a/homeassistant/components/vacuum/.translations/de.json b/homeassistant/components/vacuum/.translations/de.json new file mode 100644 index 00000000000..060358a0a7a --- /dev/null +++ b/homeassistant/components/vacuum/.translations/de.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "condtion_type": { + "is_cleaning": "{entity_name} reinigt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/en.json b/homeassistant/components/vacuum/.translations/en.json new file mode 100644 index 00000000000..396c6a83be9 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/en.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Let {entity_name} clean", + "dock": "Let {entity_name} return to the dock" + }, + "condtion_type": { + "is_cleaning": "{entity_name} is cleaning", + "is_docked": "{entity_name} is docked" + }, + "trigger_type": { + "cleaning": "{entity_name} started cleaning", + "docked": "{entity_name} docked" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/it.json b/homeassistant/components/vacuum/.translations/it.json new file mode 100644 index 00000000000..0b879f154fa --- /dev/null +++ b/homeassistant/components/vacuum/.translations/it.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Lascia pulire {entity_name}", + "dock": "Lascia che {entity_name} ritorni alla base" + }, + "condtion_type": { + "is_cleaning": "{entity_name} sta pulendo", + "is_docked": "{entity_name} \u00e8 agganciato alla base" + }, + "trigger_type": { + "cleaning": "{entity_name} ha iniziato la pulizia", + "docked": "{entity_name} agganciato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ru.json b/homeassistant/components/vacuum/.translations/ru.json new file mode 100644 index 00000000000..3026bac3012 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_cleaning": "{entity_name} \u0434\u0435\u043b\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", + "is_docked": "{entity_name} \u0443 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/zh-Hant.json b/homeassistant/components/vacuum/.translations/zh-Hant.json new file mode 100644 index 00000000000..d6831f81873 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "clean": "\u555f\u52d5 {entity_name} \u6e05\u9664", + "dock": "\u555f\u52d5 {entity_name} \u56de\u5230\u5145\u96fb\u7ad9" + }, + "trigger_type": { + "cleaning": "{entity_name} \u958b\u59cb\u6e05\u6383", + "docked": "{entity_name} \u5df2\u56de\u5145\u96fb\u7ad9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index dabf184d7ed..557518d97e8 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -7,6 +7,12 @@ "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "title": "Benutzerprofil" + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/wled/.translations/ca.json b/homeassistant/components/wled/.translations/ca.json new file mode 100644 index 00000000000..b86eefd62c8 --- /dev/null +++ b/homeassistant/components/wled/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP" + }, + "title": "Enlla\u00e7a el teu WLED" + }, + "zeroconf_confirm": { + "title": "Dispositiu WLED descobert" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/de.json b/homeassistant/components/wled/.translations/de.json new file mode 100644 index 00000000000..f50a24eeac0 --- /dev/null +++ b/homeassistant/components/wled/.translations/de.json @@ -0,0 +1,6 @@ +{ + "config": { + "flow_title": "WLED: {name}", + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/it.json b/homeassistant/components/wled/.translations/it.json new file mode 100644 index 00000000000..03c24101c2a --- /dev/null +++ b/homeassistant/components/wled/.translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "connection_error": "Impossibile connettersi al dispositivo WLED." + }, + "error": { + "connection_error": "Impossibile connettersi al dispositivo WLED." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Host o indirizzo IP" + }, + "description": "Configura WLED per l'integrazione con Home Assistant.", + "title": "Collega il tuo WLED" + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il WLED chiamato `{name}` a Home Assistant?", + "title": "Dispositivo WLED rilevato" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/ru.json b/homeassistant/components/wled/.translations/ru.json new file mode 100644 index 00000000000..cd4c3c3b066 --- /dev/null +++ b/homeassistant/components/wled/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 WLED \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "title": "WLED" + }, + "zeroconf_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 WLED `{name}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 WLED" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/zh-Hant.json b/homeassistant/components/wled/.translations/zh-Hant.json new file mode 100644 index 00000000000..b72ef3d078c --- /dev/null +++ b/homeassistant/components/wled/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "WLED \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "connection_error": "WLED \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "error": { + "connection_error": "WLED \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "flow_title": "WLED\uff1a{name}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740" + }, + "description": "\u8a2d\u5b9a WLED \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7d50 WLED" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u8a2d\u5099\u81f3 Home Assistant\uff1f", + "title": "\u767c\u73fe\u5230 WLED \u8a2d\u5099" + } + }, + "title": "WLED" + } +} \ No newline at end of file From 4f56f4e7e940a6c90f4e5877a551b73712037d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 8 Nov 2019 10:19:59 +0200 Subject: [PATCH 225/306] Add Huawei LTE device registry support (#28594) --- .../components/huawei_lte/__init__.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 112fcb6ec52..fa1423edcca 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -6,7 +6,7 @@ from functools import partial from urllib.parse import urlparse import ipaddress import logging -from typing import Any, Callable, Dict, List, Set +from typing import Any, Callable, Dict, List, Set, Tuple import voluptuous as vol import attr @@ -36,6 +36,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -135,6 +136,11 @@ class Router: pass return DEFAULT_DEVICE_NAME + @property + def device_connections(self) -> Set[Tuple[str, str]]: + """Get router connections for device registry.""" + return {(dr.CONNECTION_NETWORK_MAC, self.mac)} + def update(self) -> None: """Update router data.""" @@ -283,6 +289,30 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # Clear all subscriptions, enabled entities will push back theirs router.subscriptions.clear() + # Set up device registry + device_data = {} + sw_version = None + if router.data.get(KEY_DEVICE_INFORMATION): + device_info = router.data[KEY_DEVICE_INFORMATION] + serial_number = device_info.get("SerialNumber") + if serial_number: + device_data["identifiers"] = {(DOMAIN, serial_number)} + sw_version = device_info.get("SoftwareVersion") + if device_info.get("DeviceName"): + device_data["model"] = device_info["DeviceName"] + if not sw_version and router.data.get(KEY_DEVICE_BASIC_INFORMATION): + sw_version = router.data[KEY_DEVICE_BASIC_INFORMATION].get("SoftwareVersion") + if sw_version: + device_data["sw_version"] = sw_version + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections=router.device_connections, + name=router.device_name, + manufacturer="Huawei", + **device_data, + ) + # Forward config entry setup to platforms for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): hass.async_create_task( @@ -408,6 +438,11 @@ class HuaweiLteBaseEntity(Entity): """Huawei LTE entities report their state without polling.""" return False + @property + def device_info(self) -> Dict[str, Any]: + """Get info for matching with parent router.""" + return {"connections": self.router.device_connections} + async def async_update(self) -> None: """Update state.""" raise NotImplementedError From e96b5ef2b016e3dbea37ead117c47b1a6a8ffc59 Mon Sep 17 00:00:00 2001 From: akasma74 Date: Fri, 8 Nov 2019 08:25:37 +0000 Subject: [PATCH 226/306] Fix generic_thermostat too_hot/too_cold (#27860) * fix for too_hot/too_cold Closes #27802 * too_hot correction --- homeassistant/components/generic_thermostat/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 13e23b962c9..b765dbbfda4 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -412,8 +412,8 @@ class GenericThermostat(ClimateDevice, RestoreEntity): if not long_enough: return - too_cold = self._target_temp - self._cur_temp >= self._cold_tolerance - too_hot = self._cur_temp - self._target_temp >= self._hot_tolerance + too_cold = self._target_temp >= self._cur_temp + self._cold_tolerance + too_hot = self._cur_temp >= self._target_temp + self._hot_tolerance if self._is_device_active: if (self.ac_mode and too_cold) or (not self.ac_mode and too_hot): _LOGGER.info("Turning off heater %s", self.heater_entity_id) From b2071b81c141eeb88519ded7ee7862e32b061820 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Nov 2019 09:48:46 +0100 Subject: [PATCH 227/306] Add switch platform to WLED integration (#28606) * Add switch platform to WLED integration * Use async_schedule_update_ha_state in async context * Process review comments --- homeassistant/components/wled/__init__.py | 14 +- homeassistant/components/wled/const.py | 2 + homeassistant/components/wled/switch.py | 175 ++++++++++++++++++++ tests/components/wled/test_switch.py | 187 ++++++++++++++++++++++ 4 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/wled/switch.py create mode 100644 tests/components/wled/test_switch.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 62f611b18ec..054c09eb971 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,4 +1,5 @@ """Support for WLED.""" +import asyncio from datetime import timedelta import logging from typing import Any, Dict, Optional, Union @@ -6,6 +7,7 @@ from typing import Any, Dict, Optional, Union from wled import WLED, WLEDConnectionError, WLEDError from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.core import HomeAssistant, callback @@ -58,9 +60,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} # Set up all platforms for this device/entry. - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) - ) + for component in LIGHT_DOMAIN, SWITCH_DOMAIN: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) async def interval_update(now: dt_util.dt.datetime = None) -> None: """Poll WLED device function, dispatches event after update.""" @@ -89,7 +92,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: cancel_timer() # Unload entities for this entry/device. - await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) + await asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN), + hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), + ) # Cleanup del hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 9bc5f64a444..0836c801632 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -11,6 +11,7 @@ DATA_WLED_UPDATED = "wled_updated" # Attributes ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" +ATTR_FADE = "fade" ATTR_IDENTIFIERS = "identifiers" ATTR_INTENSITY = "intensity" ATTR_MANUFACTURER = "manufacturer" @@ -23,3 +24,4 @@ ATTR_SEGMENT_ID = "segment_id" ATTR_SOFTWARE_VERSION = "sw_version" ATTR_SPEED = "speed" ATTR_TARGET_BRIGHTNESS = "target_brightness" +ATTR_UDP_PORT = "udp_port" diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py new file mode 100644 index 00000000000..dcb41a1e49b --- /dev/null +++ b/homeassistant/components/wled/switch.py @@ -0,0 +1,175 @@ +"""Support for WLED switches.""" +import logging +from typing import Any, Callable, List + +from wled import WLED, WLEDError + +from homeassistant.components.switch import SwitchDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from . import WLEDDeviceEntity +from .const import ( + ATTR_DURATION, + ATTR_FADE, + ATTR_TARGET_BRIGHTNESS, + ATTR_UDP_PORT, + DATA_WLED_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up WLED switch based on a config entry.""" + wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + + switches = [ + WLEDNightlightSwitch(entry.entry_id, wled), + WLEDSyncSendSwitch(entry.entry_id, wled), + WLEDSyncReceiveSwitch(entry.entry_id, wled), + ] + async_add_entities(switches, True) + + +class WLEDSwitch(WLEDDeviceEntity, SwitchDevice): + """Defines a WLED switch.""" + + def __init__( + self, entry_id: str, wled: WLED, name: str, icon: str, key: str + ) -> None: + """Initialize WLED switch.""" + self._key = key + self._state = False + super().__init__(entry_id, wled, name, icon) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.wled.device.info.mac_address}_{self._key}" + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self._state + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + try: + await self._wled_turn_off() + self._state = False + except WLEDError: + _LOGGER.error("An error occurred while turning off WLED switch.") + self._available = False + self.async_schedule_update_ha_state() + + async def _wled_turn_off(self) -> None: + """Turn off the switch.""" + raise NotImplementedError() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + try: + await self._wled_turn_on() + self._state = True + except WLEDError: + _LOGGER.error("An error occurred while turning on WLED switch") + self._available = False + self.async_schedule_update_ha_state() + + async def _wled_turn_on(self) -> None: + """Turn on the switch.""" + raise NotImplementedError() + + +class WLEDNightlightSwitch(WLEDSwitch): + """Defines a WLED nightlight switch.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED nightlight switch.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Nightlight", + "mdi:weather-night", + "nightlight", + ) + + async def _wled_turn_off(self) -> None: + """Turn off the WLED nightlight switch.""" + await self.wled.nightlight(on=False) + + async def _wled_turn_on(self) -> None: + """Turn on the WLED nightlight switch.""" + await self.wled.nightlight(on=True) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.state.nightlight.on + self._attributes = { + ATTR_DURATION: self.wled.device.state.nightlight.duration, + ATTR_FADE: self.wled.device.state.nightlight.fade, + ATTR_TARGET_BRIGHTNESS: self.wled.device.state.nightlight.target_brightness, + } + + +class WLEDSyncSendSwitch(WLEDSwitch): + """Defines a WLED sync send switch.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED sync send switch.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Sync Send", + "mdi:upload-network-outline", + "sync_send", + ) + + async def _wled_turn_off(self) -> None: + """Turn off the WLED sync send switch.""" + await self.wled.sync(send=False) + + async def _wled_turn_on(self) -> None: + """Turn on the WLED sync send switch.""" + await self.wled.sync(send=True) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.state.sync.send + self._attributes = {ATTR_UDP_PORT: self.wled.device.info.udp_port} + + +class WLEDSyncReceiveSwitch(WLEDSwitch): + """Defines a WLED sync receive switch.""" + + def __init__(self, entry_id: str, wled: WLED): + """Initialize WLED sync receive switch.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Sync Receive", + "mdi:download-network-outline", + "sync_receive", + ) + + async def _wled_turn_off(self) -> None: + """Turn off the WLED sync receive switch.""" + await self.wled.sync(receive=False) + + async def _wled_turn_on(self) -> None: + """Turn on the WLED sync receive switch.""" + await self.wled.sync(receive=True) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.state.sync.receive + self._attributes = {ATTR_UDP_PORT: self.wled.device.info.udp_port} diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py new file mode 100644 index 00000000000..2dc11801712 --- /dev/null +++ b/tests/components/wled/test_switch.py @@ -0,0 +1,187 @@ +"""Tests for the WLED switch platform.""" +import aiohttp + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.wled.const import ( + ATTR_DURATION, + ATTR_FADE, + ATTR_TARGET_BRIGHTNESS, + ATTR_UDP_PORT, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_switch_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the WLED switches.""" + await init_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state + assert state.attributes.get(ATTR_DURATION) == 60 + assert state.attributes.get(ATTR_ICON) == "mdi:weather-night" + assert state.attributes.get(ATTR_TARGET_BRIGHTNESS) == 0 + assert state.attributes.get(ATTR_FADE) + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wled_rgb_light_nightlight") + assert entry + assert entry.unique_id == "aabbccddeeff_nightlight" + + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:upload-network-outline" + assert state.attributes.get(ATTR_UDP_PORT) == 21324 + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wled_rgb_light_sync_send") + assert entry + assert entry.unique_id == "aabbccddeeff_sync_send" + + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:download-network-outline" + assert state.attributes.get(ATTR_UDP_PORT) == 21324 + assert state.state == STATE_ON + + entry = entity_registry.async_get("switch.wled_rgb_light_sync_receive") + assert entry + assert entry.unique_id == "aabbccddeeff_sync_receive" + + +async def test_switch_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the change of state of the WLED switches.""" + await init_integration(hass, aioclient_mock) + + # Nightlight + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_OFF + + # Sync send + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state.state == STATE_OFF + + # Sync receive + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state.state == STATE_ON + + +async def test_switch_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the WLED switches.""" + aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError) + await init_integration(hass, aioclient_mock) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_UNAVAILABLE + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state.state == STATE_UNAVAILABLE + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state.state == STATE_UNAVAILABLE From bd54ff3c02cdc38cad647e9e658b907897f39f0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Nov 2019 01:06:16 -0800 Subject: [PATCH 228/306] Add TT WS API (#28599) * Add TT WS API * Add a test * Correctly convert TT errrors --- homeassistant/components/cloud/http_api.py | 46 ++++++++++------ homeassistant/components/cloud/manifest.json | 2 +- .../components/websocket_api/connection.py | 6 ++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_http_api.py | 53 +++++++++++++++++++ 7 files changed, 93 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 97c96b0a3e8..d969612ce8e 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -6,7 +6,7 @@ import logging import aiohttp import async_timeout import attr -from hass_nabucasa import Cloud, auth +from hass_nabucasa import Cloud, auth, thingtalk from hass_nabucasa.const import STATE_DISCONNECTED import voluptuous as vol @@ -75,28 +75,29 @@ _CLOUD_ERRORS = { async def async_setup(hass): """Initialize the HTTP API.""" - hass.components.websocket_api.async_register_command( - WS_TYPE_STATUS, websocket_cloud_status, SCHEMA_WS_STATUS - ) - hass.components.websocket_api.async_register_command( + async_register_command = hass.components.websocket_api.async_register_command + async_register_command(WS_TYPE_STATUS, websocket_cloud_status, SCHEMA_WS_STATUS) + async_register_command( WS_TYPE_SUBSCRIPTION, websocket_subscription, SCHEMA_WS_SUBSCRIPTION ) - hass.components.websocket_api.async_register_command(websocket_update_prefs) - hass.components.websocket_api.async_register_command( + async_register_command(websocket_update_prefs) + async_register_command( WS_TYPE_HOOK_CREATE, websocket_hook_create, SCHEMA_WS_HOOK_CREATE ) - hass.components.websocket_api.async_register_command( + async_register_command( WS_TYPE_HOOK_DELETE, websocket_hook_delete, SCHEMA_WS_HOOK_DELETE ) - hass.components.websocket_api.async_register_command(websocket_remote_connect) - hass.components.websocket_api.async_register_command(websocket_remote_disconnect) + async_register_command(websocket_remote_connect) + async_register_command(websocket_remote_disconnect) - hass.components.websocket_api.async_register_command(google_assistant_list) - hass.components.websocket_api.async_register_command(google_assistant_update) + async_register_command(google_assistant_list) + async_register_command(google_assistant_update) - hass.components.websocket_api.async_register_command(alexa_list) - hass.components.websocket_api.async_register_command(alexa_update) - hass.components.websocket_api.async_register_command(alexa_sync) + async_register_command(alexa_list) + async_register_command(alexa_update) + async_register_command(alexa_sync) + + async_register_command(thingtalk_convert) hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) @@ -592,3 +593,18 @@ async def alexa_sync(hass, connection, msg): connection.send_result(msg["id"]) else: connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, "Unknown error") + + +@websocket_api.async_response +@websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str}) +async def thingtalk_convert(hass, connection, msg): + """Convert a query.""" + cloud = hass.data[DOMAIN] + + with async_timeout.timeout(10): + try: + connection.send_result( + msg["id"], await thingtalk.async_convert(cloud, msg["query"]) + ) + except thingtalk.ThingTalkConversionError as err: + connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, str(err)) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 2876ff11b7e..2feef55835e 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.26"], + "requirements": ["hass-nabucasa==0.29"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 41232b097d1..5a0284a34d4 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -108,6 +108,8 @@ class ActiveConnection: @callback def async_handle_exception(self, msg, err): """Handle an exception while processing a handler.""" + log_handler = self.logger.error + if isinstance(err, Unauthorized): code = const.ERR_UNAUTHORIZED err_message = "Unauthorized" @@ -120,6 +122,8 @@ class ActiveConnection: else: code = const.ERR_UNKNOWN_ERROR err_message = "Unknown error" + log_handler = self.logger.exception + + log_handler("Error handling message: %s", err_message) - self.logger.exception("Error handling message: %s", err_message) self.send_message(messages.error_message(msg["id"], code, err_message)) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 775545c4ff1..90022bb60a0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 -hass-nabucasa==0.26 +hass-nabucasa==0.29 home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2efb1db7ba0..80d01e9ce72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.26 +hass-nabucasa==0.29 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f29eee878d9..4f4203480cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.26 +hass-nabucasa==0.29 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 314db3a9e88..8d05f1a14c3 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -7,6 +7,7 @@ import pytest from jose import jwt from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED +from hass_nabucasa import thingtalk from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth @@ -871,3 +872,55 @@ async def test_enable_alexa_state_report_fail( assert not response["success"] assert response["error"]["code"] == "alexa_relink" + + +async def test_thingtalk_convert(hass, hass_ws_client, setup_api): + """Test that we can convert a query.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.cloud.http_api.thingtalk.async_convert", + return_value=mock_coro({"hello": "world"}), + ): + await client.send_json( + {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"hello": "world"} + + +async def test_thingtalk_convert_timeout(hass, hass_ws_client, setup_api): + """Test that we can convert a query.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.cloud.http_api.thingtalk.async_convert", + side_effect=asyncio.TimeoutError, + ): + await client.send_json( + {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} + ) + response = await client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "timeout" + + +async def test_thingtalk_convert_internal(hass, hass_ws_client, setup_api): + """Test that we can convert a query.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.cloud.http_api.thingtalk.async_convert", + side_effect=thingtalk.ThingTalkConversionError("Did not understand"), + ): + await client.send_json( + {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} + ) + response = await client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + assert response["error"]["message"] == "Did not understand" From cffadf919acb11d054263dd3bdbb82f636fd7dc4 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 8 Nov 2019 17:02:44 +0100 Subject: [PATCH 229/306] Add turn_on/off to tfiac (#27712) * Add turn_on/off to tfiac * fix ws issue --- homeassistant/components/tfiac/climate.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 7979ffb9cf1..6d23018e897 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -189,3 +189,11 @@ class TfiacClimate(ClimateDevice): async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" await self._client.set_swing(swing_mode.capitalize()) + + async def async_turn_on(self): + """Turn device on.""" + await self._client.set_state(OPERATION_MODE) + + async def async_turn_off(self): + """Turn device off.""" + await self._client.set_state(ON_MODE, "off") From 4435b3a5c950ca311ddda751d0ee394704366e1b Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 8 Nov 2019 17:12:41 +0100 Subject: [PATCH 230/306] Fix issue with multiple Netatmo home coach devices (#28407) * Retrieve more detailed module infos * Switch to using IDs * Bump pyatmo version to 2.3.3 * Update requirements * Undo the change of the unique id * Rename variable --- .../components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/sensor.py | 70 +++++++++---------- requirements_all.txt | 2 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index efb2840216b..f6c08faf8fa 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==2.3.2" + "pyatmo==2.3.3" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 3d4b5f2fa51..f76062035d2 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -6,6 +6,7 @@ from time import time import pyatmo import requests +import urllib3 import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -21,7 +22,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import call_later from homeassistant.util import Throttle -from .const import DATA_NETATMO_AUTH +from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -144,37 +145,36 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def find_devices(data): """Find all devices.""" - all_module_names = data.get_module_names() + all_module_infos = data.get_module_infos() + all_module_names = [e["module_name"] for e in all_module_infos.values()] module_names = config.get(CONF_MODULES, all_module_names) - _dev = [] + entities = [] for module_name in module_names: if module_name not in all_module_names: _LOGGER.info("Module %s not found", module_name) + for module in all_module_infos.values(): + if module["module_name"] not in module_names: continue - for condition in data.station_data.monitoredConditions(module_name): + for condition in data.station_data.monitoredConditions( + moduleId=module["id"] + ): _LOGGER.debug( "Adding %s %s", - module_name, - data.station_data.moduleByName( - station=data.station, module=module_name - ), + module["module_name"], + data.station_data.moduleById(mid=module["id"]), ) - _dev.append( - NetatmoSensor( - data, module_name, condition.lower(), data.station - ) - ) - return _dev + entities.append(NetatmoSensor(data, module, condition.lower())) + return entities def _retry(_data): try: - _dev = find_devices(_data) + entities = find_devices(_data) except requests.exceptions.Timeout: return call_later( hass, NETATMO_UPDATE_INTERVAL, lambda _: _retry(_data) ) - if _dev: - add_entities(_dev, True) + if entities: + add_entities(entities, True) for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: @@ -197,22 +197,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class NetatmoSensor(Entity): """Implementation of a Netatmo sensor.""" - def __init__(self, netatmo_data, module_name, sensor_type, station): + def __init__(self, netatmo_data, module_info, sensor_type): """Initialize the sensor.""" - self._name = "Netatmo {} {}".format(module_name, SENSOR_TYPES[sensor_type][0]) self.netatmo_data = netatmo_data - self.module_name = module_name + module = self.netatmo_data.station_data.moduleById(mid=module_info["id"]) + if module["type"] == "NHC": + self.module_name = module_info["station_name"] + else: + self.module_name = module_info["module_name"] + self._name = f"{DOMAIN} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" self.type = sensor_type - self.station_name = station self._state = None self._device_class = SENSOR_TYPES[self.type][3] self._icon = SENSOR_TYPES[self.type][2] self._unit_of_measurement = SENSOR_TYPES[self.type][1] - module = self.netatmo_data.station_data.moduleByName( - station=self.station_name, module=module_name - ) self._module_type = module["type"] - self._unique_id = "{}-{}".format(module["_id"], self.type) + self._module_id = module_info["id"] + self._unique_id = f"{self._module_id}-{self.type}" @property def name(self): @@ -254,7 +255,7 @@ class NetatmoSensor(Entity): self._state = None return - data = self.netatmo_data.data.get(self.module_name) + data = self.netatmo_data.data.get(self._module_id) if data is None: _LOGGER.warning("No data found for %s", self.module_name) @@ -543,11 +544,11 @@ class NetatmoData: self._next_update = time() self._update_in_progress = threading.Lock() - def get_module_names(self): - """Return all module available on the API as a list.""" + def get_module_infos(self): + """Return all modules available on the API as a dict.""" if self.station is not None: - return self.station_data.modulesNamesList(station=self.station) - return self.station_data.modulesNamesList() + return self.station_data.getModules(station=self.station) + return self.station_data.getModules() def update(self): """Call the Netatmo API to update the data. @@ -567,14 +568,13 @@ class NetatmoData: "No Weather or HomeCoach devices found for %s", str(self.station) ) return - except requests.exceptions.Timeout: + except (requests.exceptions.Timeout, urllib3.exceptions.ReadTimeoutError): _LOGGER.warning("Timed out when connecting to Netatmo server.") return - if self.station is not None: - data = self.station_data.lastData(station=self.station, exclude=3600) - else: - data = self.station_data.lastData(exclude=3600) + data = self.station_data.lastData( + station=self.station, exclude=3600, byId=True + ) if not data: self._next_update = time() + NETATMO_UPDATE_INTERVAL return diff --git a/requirements_all.txt b/requirements_all.txt index 80d01e9ce72..a114df06a2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.3.2 +pyatmo==2.3.3 # homeassistant.components.atome pyatome==0.1.1 From 28c6837f00b32fcf60df2fd0d1536aaf92f39b58 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 Nov 2019 18:06:23 +0100 Subject: [PATCH 231/306] Add attribution and onboarding commands to conversation and Almond (#28621) * Add attribution and onboarding commands to conversation and Almond * False -> None * Comments * Update __init__.py * Comments + websocket for convert * Lint --- homeassistant/components/almond/__init__.py | 37 +++++- .../components/conversation/__init__.py | 111 +++++++++++++----- .../components/conversation/agent.py | 13 ++ 3 files changed, 128 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 5eb305e6795..9977d48ae9a 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -10,6 +10,7 @@ from aiohttp import ClientSession, ClientError from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI import voluptuous as vol +from homeassistant import core from homeassistant.const import CONF_TYPE, CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.auth.const import GROUP_ID_ADMIN @@ -95,9 +96,9 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Almond config entry.""" websession = aiohttp_client.async_get_clientsession(hass) + if entry.data["type"] == TYPE_LOCAL: auth = AlmondLocalAuth(entry.data["host"], websession) - else: # OAuth2 implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -109,7 +110,7 @@ async def async_setup_entry(hass, entry): auth = AlmondOAuth(entry.data["host"], websession, oauth_session) api = WebAlmondAPI(auth) - agent = AlmondAgent(api) + agent = AlmondAgent(hass, api, entry) # Hass.io does its own configuration of Almond. if entry.data.get("is_hassio") or entry.data["type"] != TYPE_LOCAL: @@ -202,9 +203,39 @@ class AlmondOAuth(AbstractAlmondWebAuth): class AlmondAgent(conversation.AbstractConversationAgent): """Almond conversation agent.""" - def __init__(self, api: WebAlmondAPI): + def __init__(self, hass: core.HomeAssistant, api: WebAlmondAPI, entry): """Initialize the agent.""" + self.hass = hass self.api = api + self.entry = entry + + @property + def attribution(self): + """Return the attribution.""" + return {"name": "Powered by Almond", "url": "https://almond.stanford.edu/"} + + async def async_get_onboarding(self): + """Get onboard url if not onboarded.""" + if self.entry.data.get("onboarded"): + return None + + host = self.entry.data["host"] + if self.entry.data.get("is_hassio"): + host = "/core_almond" + elif self.entry.data["type"] != TYPE_LOCAL: + host = f"{host}/me" + return { + "text": "Would you like to opt-in to share your anonymized commands with Stanford to improve Almond's responses?", + "url": f"{host}/conversation", + } + + async def async_set_onboarding(self, shown): + """Set onboarding status.""" + self.hass.config_entries.async_update_entry( + self.entry, data={**self.entry.data, "onboarded": shown} + ) + + return True async def async_process( self, text: str, conversation_id: Optional[str] = None diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index f875ec2822c..a82034a4237 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -5,7 +5,7 @@ import re import voluptuous as vol from homeassistant import core -from homeassistant.components import http +from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.helpers import config_validation as cv, intent from homeassistant.loader import bind_hass @@ -21,6 +21,7 @@ DOMAIN = "conversation" REGEX_TYPE = type(re.compile("")) DATA_AGENT = "conversation_agent" +DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" @@ -39,7 +40,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) - async_register = bind_hass(async_register) # pylint: disable=invalid-name @@ -50,18 +50,19 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): hass.data[DATA_AGENT] = agent +async def get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: + """Get agent.""" + agent = hass.data.get(DATA_AGENT) + if agent is None: + agent = hass.data[DATA_AGENT] = DefaultAgent(hass) + await agent.async_initialize(hass.data.get(DATA_CONFIG)) + return agent + + async def async_setup(hass, config): """Register the process service.""" - async def process(hass, text, conversation_id): - """Process a line of text.""" - agent = hass.data.get(DATA_AGENT) - - if agent is None: - agent = hass.data[DATA_AGENT] = DefaultAgent(hass) - await agent.async_initialize(config) - - return await agent.async_process(text, conversation_id) + hass.data[DATA_CONFIG] = config async def handle_service(service): """Parse text into commands.""" @@ -75,39 +76,89 @@ async def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA ) - - hass.http.register_view(ConversationProcessView(process)) + hass.http.register_view(ConversationProcessView()) + hass.components.websocket_api.async_register_command(websocket_process) + hass.components.websocket_api.async_register_command(websocket_get_agent_info) + hass.components.websocket_api.async_register_command(websocket_set_onboarding) return True +async def process(hass: core.HomeAssistant, text: str, conversation_id: str): + """Process text and get intent.""" + agent = await get_agent(hass) + return await agent.async_process(text, conversation_id) + + +async def get_intent(hass: core.HomeAssistant, text: str, conversation_id: str): + """Process text and get intent.""" + try: + intent_result = await process(hass, text, conversation_id) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I didn't understand that") + + return intent_result + + +@websocket_api.async_response +@websocket_api.websocket_command( + {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} +) +async def websocket_process(hass, connection, msg): + """Process text.""" + connection.send_result( + msg["id"], await get_intent(hass, msg["text"], msg.get("conversation_id")) + ) + + +@websocket_api.async_response +@websocket_api.websocket_command({"type": "conversation/agent/info"}) +async def websocket_get_agent_info(hass, connection, msg): + """Do we need onboarding.""" + agent = await get_agent(hass) + + connection.send_result( + msg["id"], + { + "onboarding": await agent.async_get_onboarding(), + "attribution": agent.attribution, + }, + ) + + +@websocket_api.async_response +@websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) +async def websocket_set_onboarding(hass, connection, msg): + """Set onboarding status.""" + agent = await get_agent(hass) + + success = await agent.async_set_onboarding(msg["shown"]) + + if success: + connection.send_result(msg["id"]) + else: + connection.send_error(msg["id"]) + + class ConversationProcessView(http.HomeAssistantView): - """View to retrieve shopping list content.""" + """View to process text.""" url = "/api/conversation/process" name = "api:conversation:process" - def __init__(self, process): - """Initialize the conversation process view.""" - self._process = process - @RequestDataValidator( vol.Schema({vol.Required("text"): str, vol.Optional("conversation_id"): str}) ) async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] - - try: - intent_result = await self._process( - hass, data["text"], data.get("conversation_id") - ) - except intent.IntentHandleError as err: - intent_result = intent.IntentResponse() - intent_result.async_set_speech(str(err)) - - if intent_result is None: - intent_result = intent.IntentResponse() - intent_result.async_set_speech("Sorry, I didn't understand that") + intent_result = await get_intent( + hass, data["text"], data.get("conversation_id") + ) return self.json(intent_result) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 1875ab5b9b9..0c47d615645 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -8,6 +8,19 @@ from homeassistant.helpers import intent class AbstractConversationAgent(ABC): """Abstract conversation agent.""" + @property + def attribution(self): + """Return the attribution.""" + return None + + async def async_get_onboarding(self): + """Get onboard data.""" + return None + + async def async_set_onboarding(self, shown): + """Set onboard data.""" + return True + @abstractmethod async def async_process( self, text: str, conversation_id: Optional[str] = None From d0f1e9fc0163f1b30ec612544956d79fd702038b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 Nov 2019 18:12:20 +0100 Subject: [PATCH 232/306] Updated frontend to 20191108.0 (#28638) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index aa7ad8b18f9..8f7212209cf 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191025.1" + "home-assistant-frontend==20191108.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 90022bb60a0..ade3305edc9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191025.1 +home-assistant-frontend==20191108.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index a114df06a2a..308c22145a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.1 +home-assistant-frontend==20191108.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f4203480cd..993dd37f43a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.1 +home-assistant-frontend==20191108.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From f2c56cff430e32f657a0f15a6ce6a3aaa76b1e5c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 8 Nov 2019 12:12:50 -0500 Subject: [PATCH 233/306] Bump ZHA quirks version (#28636) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9821ec2025b..a8ef269e394 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.10.0", - "zha-quirks==0.0.26", + "zha-quirks==0.0.27", "zigpy-deconz==0.6.0", "zigpy-homeassistant==0.10.0", "zigpy-xbee-homeassistant==0.6.0", diff --git a/requirements_all.txt b/requirements_all.txt index 308c22145a1..29b94d40644 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2051,7 +2051,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.26 +zha-quirks==0.0.27 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 993dd37f43a..ceb63df9896 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -637,7 +637,7 @@ yahooweather==0.10 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.26 +zha-quirks==0.0.27 # homeassistant.components.zha zigpy-deconz==0.6.0 From f8d3ea20b6e7495f508673fb797dde72868f605f Mon Sep 17 00:00:00 2001 From: Tomasz Date: Fri, 8 Nov 2019 18:32:44 +0100 Subject: [PATCH 234/306] Move imports in xiaomi_miio (#27773) * move imports in xiaomi_miio * reorder imports with isort * fix pylint error * Rename imports --- .../components/xiaomi_miio/device_tracker.py | 5 +- homeassistant/components/xiaomi_miio/fan.py | 94 ++++++++----------- homeassistant/components/xiaomi_miio/light.py | 38 ++------ .../components/xiaomi_miio/remote.py | 24 ++--- .../components/xiaomi_miio/sensor.py | 5 +- .../components/xiaomi_miio/switch.py | 30 ++---- .../components/xiaomi_miio/vacuum.py | 25 ++--- 7 files changed, 78 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index e2611b52f12..ef527d0aa40 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -1,6 +1,7 @@ """Support for Xiaomi Mi WiFi Repeater 2.""" import logging +from miio import DeviceException, WifiRepeater # pylint: disable=import-error import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -23,8 +24,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(hass, config): """Return a Xiaomi MiIO device scanner.""" - from miio import WifiRepeater, DeviceException - scanner = None host = config[DOMAIN][CONF_HOST] token = config[DOMAIN][CONF_TOKEN] @@ -56,8 +55,6 @@ class XiaomiMiioDeviceScanner(DeviceScanner): async def async_scan_devices(self): """Scan for devices and return a list containing found device IDs.""" - from miio import DeviceException - devices = [] try: station_info = await self.hass.async_add_executor_job(self.device.status) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index e6c356b7338..9e496893d56 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -5,19 +5,39 @@ from functools import partial import logging import voluptuous as vol +from miio import ( # pylint: disable=import-error + AirFresh, + AirHumidifier, + AirPurifier, + Device, + DeviceException, +) + +from miio.airfresh import ( # pylint: disable=import-error; pylint: disable=import-error + LedBrightness as AirfreshLedBrightness, + OperationMode as AirfreshOperationMode, +) +from miio.airhumidifier import ( # pylint: disable=import-error; pylint: disable=import-error + LedBrightness as AirhumidifierLedBrightness, + OperationMode as AirhumidifierOperationMode, +) +from miio.airpurifier import ( # pylint: disable=import-error; pylint: disable=import-error + LedBrightness as AirpurifierLedBrightness, + OperationMode as AirpurifierOperationMode, +) from homeassistant.components.fan import ( - FanEntity, + DOMAIN, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, - DOMAIN, + FanEntity, ) from homeassistant.const import ( - ATTR_MODE, - CONF_NAME, - CONF_HOST, - CONF_TOKEN, ATTR_ENTITY_ID, + ATTR_MODE, + CONF_HOST, + CONF_NAME, + CONF_TOKEN, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -429,8 +449,6 @@ SERVICE_TO_METHOD = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the miio fan device from config.""" - from miio import Device, DeviceException - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -458,18 +476,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= raise PlatformNotReady if model.startswith("zhimi.airpurifier."): - from miio import AirPurifier - air_purifier = AirPurifier(host, token) device = XiaomiAirPurifier(name, air_purifier, model, unique_id) elif model.startswith("zhimi.humidifier."): - from miio import AirHumidifier - air_humidifier = AirHumidifier(host, token, model=model) device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id) elif model.startswith("zhimi.airfresh."): - from miio import AirFresh - air_fresh = AirFresh(host, token) device = XiaomiAirFresh(name, air_fresh, model, unique_id) else: @@ -580,8 +592,6 @@ class XiaomiGenericDevice(FanEntity): async def _try_command(self, mask_error, func, *args, **kwargs): """Call a miio device command handling error messages.""" - from miio import DeviceException - try: result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) @@ -698,8 +708,6 @@ class XiaomiAirPurifier(XiaomiGenericDevice): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -731,9 +739,7 @@ class XiaomiAirPurifier(XiaomiGenericDevice): def speed(self): """Return the current speed.""" if self._state: - from miio.airpurifier import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name + return AirpurifierOperationMode(self._state_attrs[ATTR_MODE]).name return None @@ -742,14 +748,12 @@ class XiaomiAirPurifier(XiaomiGenericDevice): if self.supported_features & SUPPORT_SET_SPEED == 0: return - from miio.airpurifier import OperationMode - _LOGGER.debug("Setting the operation mode to: %s", speed) await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - OperationMode[speed.title()], + AirpurifierOperationMode[speed.title()], ) async def async_set_led_on(self): @@ -777,12 +781,10 @@ class XiaomiAirPurifier(XiaomiGenericDevice): if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: return - from miio.airpurifier import LedBrightness - await self._try_command( "Setting the led brightness of the miio device failed.", self._device.set_led_brightness, - LedBrightness(brightness), + AirpurifierLedBrightness(brightness), ) async def async_set_favorite_level(self, level: int = 1): @@ -878,21 +880,23 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): def __init__(self, name, device, model, unique_id): """Initialize the plug switch.""" - from miio.airhumidifier import OperationMode - super().__init__(name, device, model, unique_id) if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB self._speed_list = [ - mode.name for mode in OperationMode if mode is not OperationMode.Strong + mode.name + for mode in AirhumidifierOperationMode + if mode is not AirhumidifierOperationMode.Strong ] else: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER self._speed_list = [ - mode.name for mode in OperationMode if mode is not OperationMode.Auto + mode.name + for mode in AirhumidifierOperationMode + if mode is not AirhumidifierOperationMode.Auto ] self._state_attrs.update( @@ -901,8 +905,6 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -934,9 +936,7 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): def speed(self): """Return the current speed.""" if self._state: - from miio.airhumidifier import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name + return AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]).name return None @@ -945,14 +945,12 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): if self.supported_features & SUPPORT_SET_SPEED == 0: return - from miio.airhumidifier import OperationMode - _LOGGER.debug("Setting the operation mode to: %s", speed) await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - OperationMode[speed.title()], + AirhumidifierOperationMode[speed.title()], ) async def async_set_led_brightness(self, brightness: int = 2): @@ -960,12 +958,10 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: return - from miio.airhumidifier import LedBrightness - await self._try_command( "Setting the led brightness of the miio device failed.", self._device.set_led_brightness, - LedBrightness(brightness), + AirhumidifierLedBrightness(brightness), ) async def async_set_target_humidity(self, humidity: int = 40): @@ -1018,8 +1014,6 @@ class XiaomiAirFresh(XiaomiGenericDevice): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -1051,9 +1045,7 @@ class XiaomiAirFresh(XiaomiGenericDevice): def speed(self): """Return the current speed.""" if self._state: - from miio.airfresh import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name + return AirfreshOperationMode(self._state_attrs[ATTR_MODE]).name return None @@ -1062,14 +1054,12 @@ class XiaomiAirFresh(XiaomiGenericDevice): if self.supported_features & SUPPORT_SET_SPEED == 0: return - from miio.airfresh import OperationMode - _LOGGER.debug("Setting the operation mode to: %s", speed) await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - OperationMode[speed.title()], + AirfreshOperationMode[speed.title()], ) async def async_set_led_on(self): @@ -1097,12 +1087,10 @@ class XiaomiAirFresh(XiaomiGenericDevice): if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: return - from miio.airfresh import LedBrightness - await self._try_command( "Setting the led brightness of the miio device failed.", self._device.set_led_brightness, - LedBrightness(brightness), + AirfreshLedBrightness(brightness), ) async def async_set_extra_features(self, features: int = 1): diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index aa5a0ed42b9..5b454512f33 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -6,13 +6,21 @@ from functools import partial import logging from math import ceil +from miio import ( # pylint: disable=import-error + Ceil, + Device, + DeviceException, + PhilipsBulb, + PhilipsEyecare, + PhilipsMoonlight, +) import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_ENTITY_ID, + ATTR_HS_COLOR, DOMAIN, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, @@ -116,8 +124,6 @@ SERVICE_TO_METHOD = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the light from config.""" - from miio import Device, DeviceException - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -147,8 +153,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= raise PlatformNotReady if model == "philips.light.sread1": - from miio import PhilipsEyecare - light = PhilipsEyecare(host, token) primary_device = XiaomiPhilipsEyecareLamp(name, light, model, unique_id) devices.append(primary_device) @@ -161,15 +165,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # The ambient light doesn't expose additional services. # A hass.data[DATA_KEY] entry isn't needed. elif model in ["philips.light.ceiling", "philips.light.zyceiling"]: - from miio import Ceil - light = Ceil(host, token) device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device elif model == "philips.light.moonlight": - from miio import PhilipsMoonlight - light = PhilipsMoonlight(host, token) device = XiaomiPhilipsMoonlightLamp(name, light, model, unique_id) devices.append(device) @@ -180,15 +180,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "philips.light.candle2", "philips.light.downlight", ]: - from miio import PhilipsBulb - light = PhilipsBulb(host, token) device = XiaomiPhilipsBulb(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device elif model == "philips.light.mono1": - from miio import PhilipsBulb - light = PhilipsBulb(host, token) device = XiaomiPhilipsGenericLight(name, light, model, unique_id) devices.append(device) @@ -297,8 +293,6 @@ class XiaomiPhilipsAbstractLight(Light): async def _try_command(self, mask_error, func, *args, **kwargs): """Call a light command handling error messages.""" - from miio import DeviceException - try: result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) @@ -337,8 +331,6 @@ class XiaomiPhilipsAbstractLight(Light): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -363,8 +355,6 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -521,8 +511,6 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -580,8 +568,6 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -626,8 +612,6 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -769,8 +753,6 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -925,8 +907,6 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 075dd15e887..0e2ac476e05 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -1,28 +1,28 @@ """Support for the Xiaomi IR Remote (Chuangmi IR).""" import asyncio +from datetime import timedelta import logging import time -from datetime import timedelta - +from miio import ChuangmiIr, DeviceException # pylint: disable=import-error import voluptuous as vol from homeassistant.components.remote import ( - PLATFORM_SCHEMA, - DOMAIN, - ATTR_NUM_REPEATS, ATTR_DELAY_SECS, + ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, + DOMAIN, + PLATFORM_SCHEMA, RemoteDevice, ) from homeassistant.const import ( - CONF_NAME, - CONF_HOST, - CONF_TOKEN, - CONF_TIMEOUT, ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND, + CONF_HOST, + CONF_NAME, + CONF_TIMEOUT, + CONF_TOKEN, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -73,8 +73,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" - from miio import ChuangmiIr, DeviceException - host = config[CONF_HOST] token = config[CONF_TOKEN] @@ -226,8 +224,6 @@ class XiaomiMiioRemote(RemoteDevice): @property def is_on(self): """Return False if device is unreachable, else True.""" - from miio import DeviceException - try: self.device.info() return True @@ -262,8 +258,6 @@ class XiaomiMiioRemote(RemoteDevice): def _send_command(self, payload): """Send a command.""" - from miio import DeviceException - _LOGGER.debug("Sending payload: '%s'", payload) try: self.device.play(payload) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index c19e314acdd..9f5ea1fa868 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,6 +1,7 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" import logging +from miio import AirQualityMonitor, DeviceException # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -37,8 +38,6 @@ SUCCESS = ["ok"] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sensor from config.""" - from miio import AirQualityMonitor, DeviceException - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -135,8 +134,6 @@ class XiaomiAirQualityMonitor(Entity): async def async_update(self): """Fetch state from the miio device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 97e8ef27c3f..42586cd5970 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -3,6 +3,14 @@ import asyncio from functools import partial import logging +from miio import ( # pylint: disable=import-error + AirConditioningCompanionV3, + ChuangmiPlug, + Device, + DeviceException, + PowerStrip, +) +from miio.powerstrip import PowerMode # pylint: disable=import-error import voluptuous as vol from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice @@ -102,8 +110,6 @@ SERVICE_TO_METHOD = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the switch from config.""" - from miio import Device, DeviceException - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -133,8 +139,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= raise PlatformNotReady if model in ["chuangmi.plug.v1", "chuangmi.plug.v3"]: - from miio import ChuangmiPlug - plug = ChuangmiPlug(host, token, model=model) # The device has two switchable channels (mains and a USB port). @@ -145,8 +149,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.data[DATA_KEY][host] = device elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]: - from miio import PowerStrip - plug = PowerStrip(host, token, model=model) device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) @@ -157,15 +159,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "chuangmi.plug.v2", "chuangmi.plug.hmi205", ]: - from miio import ChuangmiPlug - plug = ChuangmiPlug(host, token, model=model) device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device elif model in ["lumi.acpartner.v3"]: - from miio import AirConditioningCompanionV3 - plug = AirConditioningCompanionV3(host, token) device = XiaomiAirConditioningCompanionSwitch(name, plug, model, unique_id) devices.append(device) @@ -268,8 +266,6 @@ class XiaomiPlugGenericSwitch(SwitchDevice): async def _try_command(self, mask_error, func, *args, **kwargs): """Call a plug command handling error messages.""" - from miio import DeviceException - try: result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) @@ -305,8 +301,6 @@ class XiaomiPlugGenericSwitch(SwitchDevice): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -379,8 +373,6 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -417,8 +409,6 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): if self._device_features & FEATURE_SET_POWER_MODE == 0: return - from miio.powerstrip import PowerMode - await self._try_command( "Setting the power mode of the power strip failed.", self._plug.set_power_mode, @@ -477,8 +467,6 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -538,8 +526,6 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index aa08693db63..b18a54ce97a 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -3,12 +3,19 @@ import asyncio from functools import partial import logging +from miio import DeviceException, Vacuum # pylint: disable=import-error import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, DOMAIN, PLATFORM_SCHEMA, + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, @@ -16,16 +23,10 @@ from homeassistant.components.vacuum import ( SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, - SUPPORT_STOP, - SUPPORT_STATE, SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STOP, StateVacuumDevice, - STATE_CLEANING, - STATE_DOCKED, - STATE_PAUSED, - STATE_IDLE, - STATE_RETURNING, - STATE_ERROR, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -177,8 +178,6 @@ STATE_CODE_TO_STATE = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Xiaomi vacuum cleaner robot platform.""" - from miio import Vacuum - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -348,8 +347,6 @@ class MiroboVacuum(StateVacuumDevice): async def _try_command(self, mask_error, func, *args, **kwargs): """Call a vacuum command handling error messages.""" - from miio import DeviceException - try: await self.hass.async_add_executor_job(partial(func, *args, **kwargs)) return True @@ -450,8 +447,6 @@ class MiroboVacuum(StateVacuumDevice): def update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = self._vacuum.status() self.vacuum_state = state @@ -469,8 +464,6 @@ class MiroboVacuum(StateVacuumDevice): 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) From caedc14b00044f63c79cc90eb3ef01116c6bc2e7 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Fri, 8 Nov 2019 18:48:28 +0100 Subject: [PATCH 235/306] Added support for multiple SAJ solar inverters (#28612) Changes after review --- homeassistant/components/saj/sensor.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 5605866908e..7542440c102 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, + CONF_NAME, CONF_TYPE, CONF_USERNAME, DEVICE_CLASS_POWER, @@ -48,6 +49,7 @@ SAJ_UNIT_MAPPINGS = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TYPE, default=INVERTER_TYPES[0]): vol.In(INVERTER_TYPES), vol.Inclusive(CONF_USERNAME, "credentials"): cv.string, vol.Inclusive(CONF_PASSWORD, "credentials"): cv.string, @@ -68,10 +70,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass_sensors = [] for sensor in sensor_def: - hass_sensors.append(SAJsensor(sensor)) + hass_sensors.append(SAJsensor(sensor, inverter_name=config.get(CONF_NAME))) kwargs = {} - if wifi: kwargs["wifi"] = True if config.get(CONF_USERNAME) and config.get(CONF_PASSWORD): @@ -162,14 +163,18 @@ def async_track_time_interval_backoff(hass, action) -> CALLBACK_TYPE: class SAJsensor(Entity): """Representation of a SAJ sensor.""" - def __init__(self, pysaj_sensor): + def __init__(self, pysaj_sensor, inverter_name=None): """Initialize the sensor.""" self._sensor = pysaj_sensor + self._inverter_name = inverter_name self._state = self._sensor.value @property def name(self): """Return the name of the sensor.""" + if self._inverter_name: + return f"saj_{self._inverter_name}_{self._sensor.name}" + return f"saj_{self._sensor.name}" @property @@ -230,4 +235,7 @@ class SAJsensor(Entity): @property def unique_id(self): """Return a unique identifier for this sensor.""" + if self._inverter_name: + return f"{self._inverter_name}_{self._sensor.name}" + return f"{self._sensor.name}" From 504ad6488cf214da4e45e51889c1241ee0996ddf Mon Sep 17 00:00:00 2001 From: Ari Date: Fri, 8 Nov 2019 13:08:50 -0500 Subject: [PATCH 236/306] Add support for Heat Mode detection for ecobee Heat Pumps (#28273) * Add support for Heat Mode detection for Heat Pumps - Fixes #26547 Since the ecobee component started to dynamically set the supported HVAC modes based on querying the device a few releases ago, users with Heat Pumps noticed that the Heat mode was no longer offered as an option by HA. Some of us did not actually notice until the summer was over :). This commit fixes that. For heatpumps, ecobee returns: 'coolStages': 1, 'heatStages': 0, 'hasHeatPump': True, Fix tested on HA 100.1 and 100.3 Fixes bug https://github.com/home-assistant/home-assistant/issues/26547 * changed line formatted with black --- homeassistant/components/ecobee/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index e29e2381008..c583f9696d2 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -267,7 +267,10 @@ class Thermostat(ClimateDevice): self._last_active_hvac_mode = HVAC_MODE_AUTO self._operation_list = [] - if self.thermostat["settings"]["heatStages"]: + if ( + self.thermostat["settings"]["heatStages"] + or self.thermostat["settings"]["hasHeatPump"] + ): self._operation_list.append(HVAC_MODE_HEAT) if self.thermostat["settings"]["coolStages"]: self._operation_list.append(HVAC_MODE_COOL) From bc53e9d0c824cea2b174584bdaa683e81f0c2d02 Mon Sep 17 00:00:00 2001 From: LeoCal <25389602+LeoCal@users.noreply.github.com> Date: Fri, 8 Nov 2019 20:01:35 +0100 Subject: [PATCH 237/306] Fix unhandled exception when Swisscom Internet Box is not responsive (#28618) * Update device_tracker.py From time to time, Swisscom Internet Box fails to respond and this causes an exception, which is currently not handled by the code: Traceback (most recent call last): File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/device_tracker/setup.py", line 164, in async_device_tracker_scan found_devices = await scanner.async_scan_devices() File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/swisscom/device_tracker.py", line 46, in scan_devices self._update_info() File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/swisscom/device_tracker.py", line 67, in _update_info data = self.get_swisscom_data() File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/swisscom/device_tracker.py", line 83, in get_swisscom_data request = requests.post(url, headers=headers, data=data, timeout=10) File "/srv/homeassistant/lib/python3.7/site-packages/requests/api.py", line 116, in post return request('post', url, data=data, json=json, **kwargs) File "/srv/homeassistant/lib/python3.7/site-packages/requests/api.py", line 60, in request return session.request(method=method, url=url, **kwargs) File "/srv/homeassistant/lib/python3.7/site-packages/requests/sessions.py", line 533, in request resp = self.send(prep, **send_kwargs) File "/srv/homeassistant/lib/python3.7/site-packages/requests/sessions.py", line 686, in send r.content File "/srv/homeassistant/lib/python3.7/site-packages/requests/models.py", line 828, in content self._content = b''.join(self.iter_content(CONTENT_CHUNK_SIZE)) or b'' File "/srv/homeassistant/lib/python3.7/site-packages/requests/models.py", line 757, in generate raise ConnectionError(e) requests.exceptions.ConnectionError: HTTPConnectionPool(host='192.168.1.1', port=80): Read timed out. I've just added a try-except around the post. * Update device_tracker.py Addressed blank line issue reported by flake8 * Update device_tracker.py Fixed alignment to be Black compliant. * Update device_tracker.py Fixed one more alignment issue --- homeassistant/components/swisscom/device_tracker.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 98965af1513..adb018a4b4b 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -80,9 +80,18 @@ class SwisscomDeviceScanner(DeviceScanner): {"service":"Devices", "method":"get", "parameters":{"expression":"lan and not self"}}""" - request = requests.post(url, headers=headers, data=data, timeout=10) - devices = {} + + try: + request = requests.post(url, headers=headers, data=data, timeout=10) + except ( + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + requests.exceptions.ConnectTimeout, + ): + _LOGGER.info("No response from Swisscom Internet Box") + return devices + for device in request.json()["status"]: try: devices[device["Key"]] = { From 45b53c8e8232c8c0fd5b3dece2126f3bf7e6d6ae Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 9 Nov 2019 00:32:20 +0000 Subject: [PATCH 238/306] [ci skip] Translation update --- .../components/almond/.translations/lb.json | 3 ++- .../components/almond/.translations/no.json | 3 ++- .../components/climate/.translations/ca.json | 8 ++++++ .../components/climate/.translations/lb.json | 17 ++++++++++++ .../components/climate/.translations/no.json | 17 ++++++++++++ .../components/climate/.translations/ru.json | 17 ++++++++++++ .../climate/.translations/zh-Hant.json | 9 +++++++ .../components/deconz/.translations/no.json | 18 ++++++++++++- .../components/fan/.translations/lb.json | 16 ++++++++++++ .../components/fan/.translations/no.json | 16 ++++++++++++ .../components/fan/.translations/ru.json | 4 +++ .../components/fan/.translations/zh-Hant.json | 4 +++ .../huawei_lte/.translations/no.json | 5 +++- .../components/lock/.translations/lb.json | 4 +++ .../components/lock/.translations/no.json | 4 +++ .../components/vacuum/.translations/ca.json | 7 +++++ .../components/vacuum/.translations/lb.json | 16 ++++++++++++ .../components/vacuum/.translations/no.json | 16 ++++++++++++ .../components/vacuum/.translations/ru.json | 8 ++++++ .../vacuum/.translations/zh-Hant.json | 4 +++ .../components/wled/.translations/fr.json | 5 ++++ .../components/wled/.translations/lb.json | 26 +++++++++++++++++++ .../components/wled/.translations/no.json | 26 +++++++++++++++++++ 23 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/climate/.translations/ca.json create mode 100644 homeassistant/components/climate/.translations/lb.json create mode 100644 homeassistant/components/climate/.translations/no.json create mode 100644 homeassistant/components/climate/.translations/ru.json create mode 100644 homeassistant/components/fan/.translations/lb.json create mode 100644 homeassistant/components/fan/.translations/no.json create mode 100644 homeassistant/components/vacuum/.translations/ca.json create mode 100644 homeassistant/components/vacuum/.translations/lb.json create mode 100644 homeassistant/components/vacuum/.translations/no.json create mode 100644 homeassistant/components/wled/.translations/fr.json create mode 100644 homeassistant/components/wled/.translations/lb.json create mode 100644 homeassistant/components/wled/.translations/no.json diff --git a/homeassistant/components/almond/.translations/lb.json b/homeassistant/components/almond/.translations/lb.json index f74874d283a..30cb8b86891 100644 --- a/homeassistant/components/almond/.translations/lb.json +++ b/homeassistant/components/almond/.translations/lb.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Almond Kont konfigur\u00e9ieren.", - "cannot_connect": "Kann sech net mam Almond Server verbannen." + "cannot_connect": "Kann sech net mam Almond Server verbannen.", + "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond." }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/no.json b/homeassistant/components/almond/.translations/no.json index 37888debe78..6ea2de635b1 100644 --- a/homeassistant/components/almond/.translations/no.json +++ b/homeassistant/components/almond/.translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Du kan bare konfigurere en Almond konto.", - "cannot_connect": "Kan ikke koble til Almond-serveren." + "cannot_connect": "Kan ikke koble til Almond-serveren.", + "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond." }, "title": "Almond" } diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json new file mode 100644 index 00000000000..480d90310d9 --- /dev/null +++ b/homeassistant/components/climate/.translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", + "is_preset_mode": "{entity_name} est\u00e0 configurat/ada en un mode preestablert espec\u00edfic" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/lb.json b/homeassistant/components/climate/.translations/lb.json new file mode 100644 index 00000000000..72ab7efc623 --- /dev/null +++ b/homeassistant/components/climate/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "HVAC Modus \u00e4nnere fir {entity_name}", + "set_preset_mode": "Preset \u00e4nnere fir {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "\n{entity_name} ass op e spezifesche HVAC Modus gesat", + "is_preset_mode": "{entity_name} ass op e spezifesche preset Modus gesat" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} gemoosse Fiichtegkeet ge\u00e4nnert", + "current_temperature_changed": "{entity_name} gemoossen Temperatur ge\u00e4nnert", + "hvac_mode_changed": "{entity_name} HVAC Modus ge\u00e4nnert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/no.json b/homeassistant/components/climate/.translations/no.json new file mode 100644 index 00000000000..2d95c63a6ae --- /dev/null +++ b/homeassistant/components/climate/.translations/no.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Endre HVAC-modus p\u00e5 {entity_name}", + "set_preset_mode": "Endre forh\u00e5ndsinnstilling p\u00e5 {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} er satt til en spesifikk HVAC-modus", + "is_preset_mode": "{entity_name} er satt til en spesifikk forh\u00e5ndsinnstilt modus" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} m\u00e5lt fuktighet er endret", + "current_temperature_changed": "{entity_name} m\u00e5lt temperatur er endret", + "hvac_mode_changed": "{entity_name} HVAC-modus er endret" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/ru.json b/homeassistant/components/climate/.translations/ru.json new file mode 100644 index 00000000000..045f96137d2 --- /dev/null +++ b/homeassistant/components/climate/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"", + "set_preset_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430\u0431\u043e\u0440 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0430\u0431\u043e\u0442\u044b", + "is_preset_mode": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043d\u0430\u0431\u043e\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u043d\u043e\u0439 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0438", + "current_temperature_changed": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u043d\u043e\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", + "hvac_mode_changed": "{entity_name} \u043c\u0435\u043d\u044f\u0435\u0442 \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/zh-Hant.json b/homeassistant/components/climate/.translations/zh-Hant.json index a1d603c5552..1d39eecc056 100644 --- a/homeassistant/components/climate/.translations/zh-Hant.json +++ b/homeassistant/components/climate/.translations/zh-Hant.json @@ -3,6 +3,15 @@ "action_type": { "set_hvac_mode": "\u8b8a\u66f4 {entity_name} HVAC \u6a21\u5f0f", "set_preset_mode": "\u8b8a\u66f4 {entity_name} \u8a2d\u5b9a\u6a21\u5f0f" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a HVAC \u6a21\u5f0f", + "is_preset_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a\u8a2d\u5b9a\u6a21\u5f0f" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u91cf\u6e2c\u6fd5\u5ea6\u5df2\u8b8a\u66f4", + "current_temperature_changed": "{entity_name} \u91cf\u6e2c\u6eab\u5ea6\u5df2\u8b8a\u66f4", + "hvac_mode_changed": "{entity_name} HVAC \u6a21\u5f0f\u5df2\u8b8a\u66f4" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7db8f3f118d..2c1dd687454 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -55,10 +55,17 @@ "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6", "turn_off": "Skru av", "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { + "remote_awakened": "Enheten ble vekket", "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", - "remote_gyro_activated": "Enhet er ristet" + "remote_double_tap": "Enheten \" {subtype} \" dobbeltklikket", + "remote_falling": "Enheten er i fritt fall", + "remote_gyro_activated": "Enhet er ristet", + "remote_moved": "Enheten ble flyttet med \"{under type}\" opp", + "remote_rotate_from_side_1": "Enheten rotert fra \"side 1\" til \" {subtype} \"", + "remote_rotate_from_side_2": "Enheten rotert fra \"side 2\" til \" {subtype} \"", + "remote_rotate_from_side_3": "Enheten rotert fra \"side 3\" til \" {subtype} \"", + "remote_rotate_from_side_4": "Enheten rotert fra \"side 4\" til \" {subtype} \"", + "remote_rotate_from_side_5": "Enheten rotert fra \"side 5\" til \" {subtype} \"", + "remote_rotate_from_side_6": "Enheten rotert fra \"side 6\" til \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/fan/.translations/lb.json b/homeassistant/components/fan/.translations/lb.json new file mode 100644 index 00000000000..316a77d471d --- /dev/null +++ b/homeassistant/components/fan/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + }, + "condtion_type": { + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un" + }, + "trigger_type": { + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/no.json b/homeassistant/components/fan/.translations/no.json new file mode 100644 index 00000000000..73917ac45c4 --- /dev/null +++ b/homeassistant/components/fan/.translations/no.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ru.json b/homeassistant/components/fan/.translations/ru.json index abbecabd13f..4fd5ebe28c5 100644 --- a/homeassistant/components/fan/.translations/ru.json +++ b/homeassistant/components/fan/.translations/ru.json @@ -7,6 +7,10 @@ "condtion_type": { "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + }, + "trigger_type": { + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/zh-Hant.json b/homeassistant/components/fan/.translations/zh-Hant.json index 58496f9e9bc..4b34f6e0165 100644 --- a/homeassistant/components/fan/.translations/zh-Hant.json +++ b/homeassistant/components/fan/.translations/zh-Hant.json @@ -4,6 +4,10 @@ "turn_off": "\u95dc\u9589 {entity_name}", "turn_on": "\u958b\u555f {entity_name}" }, + "condtion_type": { + "is_off": "{entity_name} \u95dc\u9589", + "is_on": "{entity_name} \u958b\u555f" + }, "trigger_type": { "turned_off": "{entity_name} \u5df2\u95dc\u9589", "turned_on": "{entity_name} \u5df2\u958b\u555f" diff --git a/homeassistant/components/huawei_lte/.translations/no.json b/homeassistant/components/huawei_lte/.translations/no.json index d06a356e998..35a5d531c5d 100644 --- a/homeassistant/components/huawei_lte/.translations/no.json +++ b/homeassistant/components/huawei_lte/.translations/no.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Denne enheten er allerede konfigurert" + "already_configured": "Denne enheten er allerede konfigurert", + "already_in_progress": "Denne enheten blir allerede konfigurert", + "not_huawei_lte": "Ikke en Huawei LTE-enhet" }, "error": { "connection_failed": "Tilkoblingen mislyktes", + "connection_timeout": "Tilkoblingsavbrudd", "incorrect_password": "feil passord", "incorrect_username": "Feil brukernavn", "incorrect_username_or_password": "Feil brukernavn eller passord", diff --git a/homeassistant/components/lock/.translations/lb.json b/homeassistant/components/lock/.translations/lb.json index 90dd7e6087a..1bdfa9ac4ec 100644 --- a/homeassistant/components/lock/.translations/lb.json +++ b/homeassistant/components/lock/.translations/lb.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} ass gespaart", "is_unlocked": "{entity_name} ass entspaart" + }, + "trigger_type": { + "locked": "{entity_name} gespaart", + "unlocked": "{entity_name} entspaart" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/no.json b/homeassistant/components/lock/.translations/no.json index 28c809a82d1..de34f40bc38 100644 --- a/homeassistant/components/lock/.translations/no.json +++ b/homeassistant/components/lock/.translations/no.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} er l\u00e5st", "is_unlocked": "{entity_name} er l\u00e5st opp" + }, + "trigger_type": { + "locked": "{entity_name} l\u00e5st", + "unlocked": "{entity_name} l\u00e5st opp" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json new file mode 100644 index 00000000000..c004120a8c7 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "condtion_type": { + "is_cleaning": "{entity_name} est\u00e0 netejant" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/lb.json b/homeassistant/components/vacuum/.translations/lb.json new file mode 100644 index 00000000000..6d984b997fa --- /dev/null +++ b/homeassistant/components/vacuum/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Looss {entity_name} botzen", + "dock": "Sch\u00e9ck {entity_name} z\u00e9reck zur Statioun" + }, + "condtion_type": { + "is_cleaning": "{entity_name} botzt", + "is_docked": "{entity_name} ass an der Statioun" + }, + "trigger_type": { + "cleaning": "{entity_name} huet ugefaange mam botzen", + "docked": "{entity_name} an der Statioun" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/no.json b/homeassistant/components/vacuum/.translations/no.json new file mode 100644 index 00000000000..7d6475f8cef --- /dev/null +++ b/homeassistant/components/vacuum/.translations/no.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "La {entity_name} rengj\u00f8res", + "dock": "La {entity_name} tilbake til dock" + }, + "condtion_type": { + "is_cleaning": "{entity_name} rengj\u00f8res", + "is_docked": "{entity_name} er docked" + }, + "trigger_type": { + "cleaning": "{entity_name} startet rengj\u00f8ringen", + "docked": "{entity_name} dokket" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ru.json b/homeassistant/components/vacuum/.translations/ru.json index 3026bac3012..c727e8f6ea3 100644 --- a/homeassistant/components/vacuum/.translations/ru.json +++ b/homeassistant/components/vacuum/.translations/ru.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "clean": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c {entity_name} \u0434\u0435\u043b\u0430\u0442\u044c \u0443\u0431\u043e\u0440\u043a\u0443", + "dock": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c {entity_name} \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u044e" + }, "condtion_type": { "is_cleaning": "{entity_name} \u0434\u0435\u043b\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", "is_docked": "{entity_name} \u0443 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "trigger_type": { + "cleaning": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", + "docked": "{entity_name} \u0441\u0442\u044b\u043a\u0443\u0435\u0442\u0441\u044f \u0441 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0435\u0439" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/zh-Hant.json b/homeassistant/components/vacuum/.translations/zh-Hant.json index d6831f81873..f0ad431afc9 100644 --- a/homeassistant/components/vacuum/.translations/zh-Hant.json +++ b/homeassistant/components/vacuum/.translations/zh-Hant.json @@ -4,6 +4,10 @@ "clean": "\u555f\u52d5 {entity_name} \u6e05\u9664", "dock": "\u555f\u52d5 {entity_name} \u56de\u5230\u5145\u96fb\u7ad9" }, + "condtion_type": { + "is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u6383", + "is_docked": "{entity_name} \u65bc\u5145\u96fb\u7ad9" + }, "trigger_type": { "cleaning": "{entity_name} \u958b\u59cb\u6e05\u6383", "docked": "{entity_name} \u5df2\u56de\u5145\u96fb\u7ad9" diff --git a/homeassistant/components/wled/.translations/fr.json b/homeassistant/components/wled/.translations/fr.json new file mode 100644 index 00000000000..a9ef8aa567a --- /dev/null +++ b/homeassistant/components/wled/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/lb.json b/homeassistant/components/wled/.translations/lb.json new file mode 100644 index 00000000000..ea23956af42 --- /dev/null +++ b/homeassistant/components/wled/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebsen WLED Apparat ass scho konfigur\u00e9iert.", + "connection_error": "Feeler beim verbannen mam WLED Apparat." + }, + "error": { + "connection_error": "Feeler beim verbannen mam WLED Apparat." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Numm oder IP Adresse" + }, + "description": "\u00c4ren WLED als Integratioun mam Home Assistant ariichten.", + "title": "\u00c4ren WLED verbannen" + }, + "zeroconf_confirm": { + "description": "W\u00ebllt dir den WLED mam Numm `{name}` am 'Home Assistant dob\u00e4isetzen?", + "title": "Entdeckten WLED Apparat" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/no.json b/homeassistant/components/wled/.translations/no.json new file mode 100644 index 00000000000..b2dc9cb6547 --- /dev/null +++ b/homeassistant/components/wled/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Denne WLED-enheten er allerede konfigurert.", + "connection_error": "Kunne ikke koble til WLED-enheten." + }, + "error": { + "connection_error": "Kunne ikke koble til WLED-enheten." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Vert eller IP-adresse" + }, + "description": "Konfigurer WLED til \u00e5 integreres med Home Assistant.", + "title": "Linken din WLED" + }, + "zeroconf_confirm": { + "description": "Vil du legge til WLED med navnet ' {name} ' i Home Assistant?", + "title": "Oppdaget WLED-enhet" + } + }, + "title": "WLED" + } +} \ No newline at end of file From 97224df4fd9ce72d629bec322a17ef0d47881776 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Fri, 8 Nov 2019 22:35:45 -0800 Subject: [PATCH 239/306] Fix Abode capture_image and trigger_quick_action services (#28546) * Fix Abode services * Bump abodepy version * Updated using dispatcher helper * Code review fixes * Removed init method from AbodeQuickActionBinary Sensor --- homeassistant/components/abode/__init__.py | 41 ++++++++++++------- .../components/abode/alarm_control_panel.py | 3 +- .../components/abode/binary_sensor.py | 19 ++++++--- homeassistant/components/abode/camera.py | 18 ++++---- homeassistant/components/abode/const.py | 3 ++ homeassistant/components/abode/cover.py | 10 ++--- homeassistant/components/abode/light.py | 6 +-- homeassistant/components/abode/lock.py | 10 ++--- homeassistant/components/abode/manifest.json | 2 +- homeassistant/components/abode/sensor.py | 3 +- homeassistant/components/abode/switch.py | 8 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 13 files changed, 76 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 76c14d7917f..1689576bc7f 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -20,10 +20,17 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -from .const import ATTRIBUTION, DOMAIN, DEFAULT_CACHEDB +from .const import ( + ATTRIBUTION, + DOMAIN, + DEFAULT_CACHEDB, + SIGNAL_CAPTURE_IMAGE, + SIGNAL_TRIGGER_QUICK_ACTION, +) _LOGGER = logging.getLogger(__name__) @@ -89,7 +96,7 @@ class AbodeSystem: self.abode = abode self.polling = polling - self.devices = [] + self.entity_ids = set() self.logout_listener = None @@ -179,27 +186,29 @@ def setup_hass_services(hass): """Capture a new image.""" entity_ids = call.data.get(ATTR_ENTITY_ID) - target_devices = [ - device - for device in hass.data[DOMAIN].devices - if device.entity_id in entity_ids + target_entities = [ + entity_id + for entity_id in hass.data[DOMAIN].entity_ids + if entity_id in entity_ids ] - for device in target_devices: - device.capture() + for entity_id in target_entities: + signal = SIGNAL_CAPTURE_IMAGE.format(entity_id) + dispatcher_send(hass, signal) def trigger_quick_action(call): """Trigger a quick action.""" entity_ids = call.data.get(ATTR_ENTITY_ID, None) - target_devices = [ - device - for device in hass.data[DOMAIN].devices - if device.entity_id in entity_ids + target_entities = [ + entity_id + for entity_id in hass.data[DOMAIN].entity_ids + if entity_id in entity_ids ] - for device in target_devices: - device.trigger() + for entity_id in target_entities: + signal = SIGNAL_TRIGGER_QUICK_ACTION.format(entity_id) + dispatcher_send(hass, signal) hass.services.register( DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA @@ -290,6 +299,7 @@ class AbodeDevice(Entity): self._device.device_id, self._update_callback, ) + self.hass.data[DOMAIN].entity_ids.add(self.entity_id) async def async_will_remove_from_hass(self): """Unsubscribe from device events.""" @@ -352,13 +362,14 @@ class AbodeAutomation(Entity): self._event = event async def async_added_to_hass(self): - """Subscribe Abode events.""" + """Subscribe to a group of Abode timeline events.""" if self._event: self.hass.async_add_job( self._data.abode.events.add_event_callback, self._event, self._update_callback, ) + self.hass.data[DOMAIN].entity_ids.add(self.entity_id) @property def should_poll(self): diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index f774e773cb5..f1ff08f3a0a 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -23,9 +23,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up an alarm control panel for an Abode device.""" + """Set up Abode alarm control panel device.""" data = hass.data[DOMAIN] - async_add_entities( [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] ) diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 31f74448496..56c7bbcc1ff 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -5,9 +5,10 @@ import abodepy.helpers.constants as CONST import abodepy.helpers.timeline as TIMELINE from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import AbodeAutomation, AbodeDevice -from .const import DOMAIN +from .const import DOMAIN, SIGNAL_TRIGGER_QUICK_ACTION _LOGGER = logging.getLogger(__name__) @@ -18,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up a sensor for an Abode device.""" + """Set up Abode binary sensor devices.""" data = hass.data[DOMAIN] device_types = [ @@ -29,19 +30,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): CONST.TYPE_OPENING, ] - devices = [] + entities = [] for device in data.abode.get_devices(generic_type=device_types): - devices.append(AbodeBinarySensor(data, device)) + entities.append(AbodeBinarySensor(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION): - devices.append( + entities.append( AbodeQuickActionBinarySensor( data, automation, TIMELINE.AUTOMATION_EDIT_GROUP ) ) - async_add_entities(devices) + async_add_entities(entities) class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): @@ -61,6 +62,12 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice): """A binary sensor implementation for Abode quick action automations.""" + async def async_added_to_hass(self): + """Subscribe Abode events.""" + await super().async_added_to_hass() + signal = SIGNAL_TRIGGER_QUICK_ACTION.format(self.entity_id) + async_dispatcher_connect(self.hass, signal, self.trigger) + def trigger(self): """Trigger a quick automation.""" self._automation.trigger() diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index e98a59a985c..c6f366e0e51 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -7,10 +7,11 @@ import abodepy.helpers.timeline as TIMELINE import requests from homeassistant.components.camera import Camera +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import Throttle from . import AbodeDevice -from .const import DOMAIN +from .const import DOMAIN, SIGNAL_CAPTURE_IMAGE MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) @@ -23,15 +24,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up a camera for an Abode device.""" - + """Set up Abode camera devices.""" data = hass.data[DOMAIN] - devices = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) + entities = [] - async_add_entities(devices) + for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): + entities.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) + + async_add_entities(entities) class AbodeCamera(AbodeDevice, Camera): @@ -54,6 +55,9 @@ class AbodeCamera(AbodeDevice, Camera): self._capture_callback, ) + signal = SIGNAL_CAPTURE_IMAGE.format(self.entity_id) + async_dispatcher_connect(self.hass, signal, self.capture) + def capture(self): """Request a new image capture.""" return self._device.capture() diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py index 092843ba212..267eb04f72e 100644 --- a/homeassistant/components/abode/const.py +++ b/homeassistant/components/abode/const.py @@ -3,3 +3,6 @@ DOMAIN = "abode" ATTRIBUTION = "Data provided by goabode.com" DEFAULT_CACHEDB = "abodepy_cache.pickle" + +SIGNAL_CAPTURE_IMAGE = "abode_camera_capture_{}" +SIGNAL_TRIGGER_QUICK_ACTION = "abode_trigger_quick_action_{}" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index ebe59ee45c7..a4fce7e7b8a 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -18,14 +18,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode cover devices.""" - data = hass.data[DOMAIN] - devices = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - devices.append(AbodeCover(data, device)) + entities = [] - async_add_entities(devices) + for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): + entities.append(AbodeCover(data, device)) + + async_add_entities(entities) class AbodeCover(AbodeDevice, CoverDevice): diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 163982d040e..f29270d264c 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -33,12 +33,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode light devices.""" data = hass.data[DOMAIN] - devices = [] + entities = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): - devices.append(AbodeLight(data, device)) + entities.append(AbodeLight(data, device)) - async_add_entities(devices) + async_add_entities(entities) class AbodeLight(AbodeDevice, Light): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 11f792f88fd..e7ed40849de 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -18,14 +18,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode lock devices.""" - data = hass.data[DOMAIN] - devices = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - devices.append(AbodeLock(data, device)) + entities = [] - async_add_entities(devices) + for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): + entities.append(AbodeLock(data, device)) + + async_add_entities(entities) class AbodeLock(AbodeDevice, LockDevice): diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index b54120c7cbd..0a4307ff737 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ - "abodepy==0.16.6" + "abodepy==0.16.7" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 6ee0cf59cbf..d84bfe52441 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -28,8 +28,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up a sensor for an Abode device.""" + """Set up Abode sensor devices.""" data = hass.data[DOMAIN] + entities = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 7bd7f394d30..c092c1ef3f0 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -21,17 +21,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode switch devices.""" data = hass.data[DOMAIN] - devices = [] + entities = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): - devices.append(AbodeSwitch(data, device)) + entities.append(AbodeSwitch(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): - devices.append( + entities.append( AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) ) - async_add_entities(devices) + async_add_entities(entities) class AbodeSwitch(AbodeDevice, SwitchDevice): diff --git a/requirements_all.txt b/requirements_all.txt index 29b94d40644..a34191c4a5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -106,7 +106,7 @@ WazeRouteCalculator==0.10 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.6 +abodepy==0.16.7 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceb63df9896..691e8d413d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -26,7 +26,7 @@ RtmAPI==0.7.2 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.6 +abodepy==0.16.7 # homeassistant.components.androidtv adb-shell==0.0.8 From 58eeea903f6208dec607ad5b7c68a9429e3cfffe Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Sat, 9 Nov 2019 10:14:46 +0300 Subject: [PATCH 240/306] Add pcal9535a integration (#26563) * Support for PCAL9535A chip Signed-off-by: Denis Shulyaka * Code review changes * Code review changes * Fix import order * Fix import order * Apply suggestions from code review --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/pcal9535a/__init__.py | 3 + .../components/pcal9535a/binary_sensor.py | 93 ++++++++++++++++ .../components/pcal9535a/manifest.json | 10 ++ homeassistant/components/pcal9535a/switch.py | 102 ++++++++++++++++++ requirements_all.txt | 3 + 7 files changed, 213 insertions(+) create mode 100644 homeassistant/components/pcal9535a/__init__.py create mode 100644 homeassistant/components/pcal9535a/binary_sensor.py create mode 100644 homeassistant/components/pcal9535a/manifest.json create mode 100644 homeassistant/components/pcal9535a/switch.py diff --git a/.coveragerc b/.coveragerc index 169b73b7899..b0d6a40c7f7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -499,6 +499,7 @@ omit = homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py + homeassistant/components/pcal9535a/* homeassistant/components/pencom/switch.py homeassistant/components/philips_js/media_player.py homeassistant/components/pi_hole/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0a02fbc5321..cb3d0817d59 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -225,6 +225,7 @@ homeassistant/components/oru/* @bvlaicu homeassistant/components/owlet/* @oblogic7 homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend +homeassistant/components/pcal9535a/* @Shulyaka homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff @johnluetke diff --git a/homeassistant/components/pcal9535a/__init__.py b/homeassistant/components/pcal9535a/__init__.py new file mode 100644 index 00000000000..fa1295939be --- /dev/null +++ b/homeassistant/components/pcal9535a/__init__.py @@ -0,0 +1,3 @@ +"""Support for I2C PCAL9535A chip.""" + +DOMAIN = "pcal9535a" diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py new file mode 100644 index 00000000000..fd4e92ccf03 --- /dev/null +++ b/homeassistant/components/pcal9535a/binary_sensor.py @@ -0,0 +1,93 @@ +"""Support for binary sensor using I2C PCAL9535A chip.""" +import logging + +import voluptuous as vol +from pcal9535a import PCAL9535A + +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.const import DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_INVERT_LOGIC = "invert_logic" +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_PINS = "pins" +CONF_PULL_MODE = "pull_mode" + +MODE_UP = "UP" +MODE_DOWN = "DOWN" +MODE_DISABLED = "DISABLED" + +DEFAULT_INVERT_LOGIC = False +DEFAULT_I2C_ADDRESS = 0x20 +DEFAULT_I2C_BUS = 1 +DEFAULT_PULL_MODE = MODE_DISABLED + +_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PINS): _SENSORS_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.All( + vol.Upper, vol.In([MODE_UP, MODE_DOWN, MODE_DISABLED]) + ), + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the PCAL9535A binary sensors.""" + pull_mode = config[CONF_PULL_MODE] + invert_logic = config[CONF_INVERT_LOGIC] + i2c_address = config[CONF_I2C_ADDRESS] + bus = config[CONF_I2C_BUS] + + pcal = PCAL9535A(bus, i2c_address) + + binary_sensors = [] + pins = config[CONF_PINS] + + for pin_num, pin_name in pins.items(): + pin = pcal.get_pin(pin_num // 8, pin_num % 8) + binary_sensors.append( + PCAL9535ABinarySensor(pin_name, pin, pull_mode, invert_logic) + ) + + add_entities(binary_sensors, True) + + +class PCAL9535ABinarySensor(BinarySensorDevice): + """Represent a binary sensor that uses PCAL9535A.""" + + def __init__(self, name, pin, pull_mode, invert_logic): + """Initialize the PCAL9535A binary sensor.""" + self._name = name or DEVICE_DEFAULT_NAME + self._pin = pin + self._pin.input = True + self._pin.inverted = invert_logic + if pull_mode == "DISABLED": + self._pin.pullup = 0 + elif pull_mode == "DOWN": + self._pin.pullup = -1 + else: + self._pin.pullup = 1 + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the cached state of the entity.""" + return self._state + + def update(self): + """Update the GPIO state.""" + self._state = self._pin.level diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json new file mode 100644 index 00000000000..e2fb140c7a9 --- /dev/null +++ b/homeassistant/components/pcal9535a/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "pcal9535a", + "name": "PCAL9535A I/O Expander", + "documentation": "https://www.home-assistant.io/components/pcal9535a", + "requirements": [ + "pcal9535a==0.7" + ], + "dependencies": [], + "codeowners": ["@Shulyaka"] +} diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py new file mode 100644 index 00000000000..faebce5d67e --- /dev/null +++ b/homeassistant/components/pcal9535a/switch.py @@ -0,0 +1,102 @@ +"""Support for switch sensor using I2C PCAL9535A chip.""" +import logging + +import voluptuous as vol +from pcal9535a import PCAL9535A + +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_INVERT_LOGIC = "invert_logic" +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_PINS = "pins" +CONF_STRENGTH = "strength" + +STRENGTH_025 = "0.25" +STRENGTH_050 = "0.5" +STRENGTH_075 = "0.75" +STRENGTH_100 = "1.0" + +DEFAULT_INVERT_LOGIC = False +DEFAULT_I2C_ADDRESS = 0x20 +DEFAULT_I2C_BUS = 1 +DEFAULT_STRENGTH = STRENGTH_100 + +_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PINS): _SWITCHES_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_STRENGTH, default=DEFAULT_STRENGTH): vol.In( + [STRENGTH_025, STRENGTH_050, STRENGTH_075, STRENGTH_100] + ), + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the PCAL9535A devices.""" + invert_logic = config[CONF_INVERT_LOGIC] + i2c_address = config[CONF_I2C_ADDRESS] + bus = config[CONF_I2C_BUS] + + pcal = PCAL9535A(bus, i2c_address) + + switches = [] + pins = config[CONF_PINS] + for pin_num, pin_name in pins.items(): + pin = pcal.get_pin(pin_num // 8, pin_num % 8) + switches.append(PCAL9535ASwitch(pin_name, pin, invert_logic)) + + add_entities(switches) + + +class PCAL9535ASwitch(SwitchDevice): + """Representation of a PCAL9535A output pin.""" + + def __init__(self, name, pin, invert_logic): + """Initialize the pin.""" + self._name = name or DEVICE_DEFAULT_NAME + self._pin = pin + self._pin.inverted = invert_logic + self._pin.input = False + self._state = self._pin.level + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def assumed_state(self): + """Return true if optimistic updates are used.""" + return True + + def turn_on(self, **kwargs): + """Turn the device on.""" + self._pin.level = True + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._pin.level = False + self._state = False + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index a34191c4a5c..6f0b40230ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -944,6 +944,9 @@ panacotta==0.1 # homeassistant.components.panasonic_viera panasonic_viera==0.3.2 +# homeassistant.components.pcal9535a +pcal9535a==0.7 + # homeassistant.components.dunehd pdunehd==1.3 From 0954f7d3bef313686686db3ee539086ee441fc1e Mon Sep 17 00:00:00 2001 From: bluestripe Date: Sat, 9 Nov 2019 08:16:53 +0100 Subject: [PATCH 241/306] Add bluesound speaker group attribute (#28142) * Added bluesound speaker group attribute. * Changed code to fix failing tests. * Changed condition checking for empty group list. * Investigating CI pipeline error * Changed back to the code that passed CI earlier * Changed condition on existence of list and sorting of bluesound_group * Re-introduced guard on group_name None --- .../components/bluesound/media_player.py | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 762f231b341..7b2719c1e4e 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -53,6 +53,7 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +ATTR_BLUESOUND_GROUP = "bluesound_group" ATTR_MASTER = "master" DATA_BLUESOUND = "bluesound" @@ -219,6 +220,8 @@ class BluesoundPlayer(MediaPlayerDevice): self._master = None self._is_master = False self._group_name = None + self._group_list = [] + self._bluesound_device_name = None self._init_callback = init_callback if self.port is None: @@ -247,6 +250,8 @@ class BluesoundPlayer(MediaPlayerDevice): if not self._name: self._name = self._sync_status.get("@name", self.host) + if not self._bluesound_device_name: + self._bluesound_device_name = self._sync_status.get("@name", self.host) if not self._icon: self._icon = self._sync_status.get("@icon", self.host) @@ -331,7 +336,6 @@ class BluesoundPlayer(MediaPlayerDevice): self, method, raise_timeout=False, allow_offline=False ): """Send command to the player.""" - if not self._is_online and not allow_offline: return @@ -371,7 +375,6 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_update_status(self): """Use the poll session to always get the status of the player.""" - response = None url = "Status" @@ -402,6 +405,10 @@ class BluesoundPlayer(MediaPlayerDevice): if group_name != self._group_name: _LOGGER.debug("Group name change detected on device: %s", self.host) self._group_name = group_name + + # rebuild ordered list of entity_ids that are in the group, master is first + self._group_list = self.rebuild_bluesound_group() + # the sleep is needed to make sure that the # devices is synced await asyncio.sleep(1) @@ -659,6 +666,11 @@ class BluesoundPlayer(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def bluesound_device_name(self): + """Return the device name as returned by the device.""" + return self._bluesound_device_name + @property def icon(self): """Return the icon of the device.""" @@ -690,7 +702,6 @@ class BluesoundPlayer(MediaPlayerDevice): @property def source(self): """Name of the current input source.""" - if self._status is None or (self.is_grouped and not self.is_master): return None @@ -831,6 +842,39 @@ class BluesoundPlayer(MediaPlayerDevice): else: _LOGGER.error("Master not found %s", master_device) + @property + def device_state_attributes(self): + """List members in group.""" + attributes = {} + if self._group_list: + attributes = {ATTR_BLUESOUND_GROUP: self._group_list} + + attributes[ATTR_MASTER] = self._is_master + + return attributes + + def rebuild_bluesound_group(self): + """Rebuild the list of entities in speaker group.""" + if self._group_name is None: + return None + + bluesound_group = [] + + device_group = self._group_name.split("+") + + sorted_entities = sorted( + self._hass.data[DATA_BLUESOUND], + key=lambda entity: entity.is_master, + reverse=True, + ) + bluesound_group = [ + entity.name + for entity in sorted_entities + if entity.bluesound_device_name in device_group + ] + + return bluesound_group + async def async_unjoin(self): """Unjoin the player from a group.""" if self._master is None: From fc95a3d0889d7e442d55ff331d5252406ec74e06 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 9 Nov 2019 14:07:01 +0100 Subject: [PATCH 242/306] Fix xiaomi vacuum tests (#28658) * Fix xiaomi exceptions test * Fix xiaomi_specific_services test * Fix remaining xiaomi miio vacuum tests * Clean up --- tests/components/xiaomi_miio/test_vacuum.py | 290 ++++++++++---------- 1 file changed, 150 insertions(+), 140 deletions(-) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index ceeb7a92615..18da270960c 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -15,11 +15,10 @@ from homeassistant.components.vacuum import ( SERVICE_RETURN_TO_BASE, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, - SERVICE_START_PAUSE, + SERVICE_START, SERVICE_STOP, - SERVICE_TOGGLE, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, + STATE_CLEANING, + STATE_ERROR, ) from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_CLEANED_AREA, @@ -55,91 +54,97 @@ from homeassistant.setup import async_setup_component PLATFORM = "xiaomi_miio" # calls made when device status is requested -status_calls = [ - mock.call.Vacuum().status(), - mock.call.Vacuum().consumable_status(), - mock.call.Vacuum().clean_history(), - mock.call.Vacuum().dnd_status(), +STATUS_CALLS = [ + mock.call.status(), + mock.call.consumable_status(), + mock.call.clean_history(), + mock.call.dnd_status(), ] -@pytest.fixture -def mock_mirobo_is_off(): +@pytest.fixture(name="mock_mirobo_is_got_error") +def mirobo_is_got_error_fixture(): """Mock mock_mirobo.""" mock_vacuum = mock.MagicMock() - mock_vacuum.Vacuum().status().data = {"test": "raw"} - mock_vacuum.Vacuum().status().is_on = False - mock_vacuum.Vacuum().status().fanspeed = 38 - mock_vacuum.Vacuum().status().got_error = True - mock_vacuum.Vacuum().status().error = "Error message" - mock_vacuum.Vacuum().status().battery = 82 - mock_vacuum.Vacuum().status().clean_area = 123.43218 - mock_vacuum.Vacuum().status().clean_time = timedelta( - hours=2, minutes=35, seconds=34 - ) - mock_vacuum.Vacuum().consumable_status().main_brush_left = timedelta( + mock_vacuum.status().data = {"test": "raw"} + mock_vacuum.status().is_on = False + mock_vacuum.status().fanspeed = 38 + mock_vacuum.status().got_error = True + mock_vacuum.status().error = "Error message" + mock_vacuum.status().battery = 82 + mock_vacuum.status().clean_area = 123.43218 + mock_vacuum.status().clean_time = timedelta(hours=2, minutes=35, seconds=34) + mock_vacuum.consumable_status().main_brush_left = timedelta( hours=12, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().consumable_status().side_brush_left = timedelta( + mock_vacuum.consumable_status().side_brush_left = timedelta( hours=12, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().consumable_status().filter_left = timedelta( + mock_vacuum.consumable_status().filter_left = timedelta( hours=12, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().clean_history().count = "35" - mock_vacuum.Vacuum().clean_history().total_area = 123.43218 - mock_vacuum.Vacuum().clean_history().total_duration = timedelta( + mock_vacuum.clean_history().count = "35" + mock_vacuum.clean_history().total_area = 123.43218 + mock_vacuum.clean_history().total_duration = timedelta( hours=11, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().status().state = "Test Xiaomi Charging" - mock_vacuum.Vacuum().dnd_status().enabled = True - mock_vacuum.Vacuum().dnd_status().start = time(hour=22, minute=0) - mock_vacuum.Vacuum().dnd_status().end = time(hour=6, minute=0) + mock_vacuum.status().state = "Test Xiaomi Charging" + mock_vacuum.dnd_status().enabled = True + mock_vacuum.dnd_status().start = time(hour=22, minute=0) + mock_vacuum.dnd_status().end = time(hour=6, minute=0) - with mock.patch.dict("sys.modules", {"miio": mock_vacuum}): + with mock.patch( + "homeassistant.components.xiaomi_miio.vacuum.Vacuum" + ) as mock_vaccum_cls: + mock_vaccum_cls.return_value = mock_vacuum yield mock_vacuum -@pytest.fixture -def mock_mirobo_is_on(): +@pytest.fixture(name="mock_mirobo_is_on") +def mirobo_is_on_fixture(): """Mock mock_mirobo.""" mock_vacuum = mock.MagicMock() - mock_vacuum.Vacuum().status().data = {"test": "raw"} - mock_vacuum.Vacuum().status().is_on = True - mock_vacuum.Vacuum().status().fanspeed = 99 - mock_vacuum.Vacuum().status().got_error = False - mock_vacuum.Vacuum().status().battery = 32 - mock_vacuum.Vacuum().status().clean_area = 133.43218 - mock_vacuum.Vacuum().status().clean_time = timedelta( - hours=2, minutes=55, seconds=34 - ) - mock_vacuum.Vacuum().consumable_status().main_brush_left = timedelta( + mock_vacuum.status().data = {"test": "raw"} + mock_vacuum.status().is_on = True + mock_vacuum.status().fanspeed = 99 + mock_vacuum.status().got_error = False + mock_vacuum.status().battery = 32 + mock_vacuum.status().clean_area = 133.43218 + mock_vacuum.status().clean_time = timedelta(hours=2, minutes=55, seconds=34) + mock_vacuum.consumable_status().main_brush_left = timedelta( hours=11, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().consumable_status().side_brush_left = timedelta( + mock_vacuum.consumable_status().side_brush_left = timedelta( hours=11, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().consumable_status().filter_left = timedelta( + mock_vacuum.consumable_status().filter_left = timedelta( hours=11, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().clean_history().count = "41" - mock_vacuum.Vacuum().clean_history().total_area = 323.43218 - mock_vacuum.Vacuum().clean_history().total_duration = timedelta( + mock_vacuum.clean_history().count = "41" + mock_vacuum.clean_history().total_area = 323.43218 + mock_vacuum.clean_history().total_duration = timedelta( hours=11, minutes=15, seconds=34 ) - mock_vacuum.Vacuum().status().state = "Test Xiaomi Cleaning" - mock_vacuum.Vacuum().dnd_status().enabled = False + mock_vacuum.status().state = "Test Xiaomi Cleaning" + mock_vacuum.status().state_code = 5 + mock_vacuum.dnd_status().enabled = False - with mock.patch.dict("sys.modules", {"miio": mock_vacuum}): + with mock.patch( + "homeassistant.components.xiaomi_miio.vacuum.Vacuum" + ) as mock_vaccum_cls: + mock_vaccum_cls.return_value = mock_vacuum yield mock_vacuum -@pytest.fixture -def mock_mirobo_errors(): +@pytest.fixture(name="mock_mirobo_errors") +def mirobo_errors_fixture(): """Mock mock_mirobo_errors to simulate a bad vacuum status request.""" mock_vacuum = mock.MagicMock() - mock_vacuum.Vacuum().status.side_effect = OSError() - with mock.patch.dict("sys.modules", {"miio": mock_vacuum}): + mock_vacuum.status.side_effect = OSError() + with mock.patch( + "homeassistant.components.xiaomi_miio.vacuum.Vacuum" + ) as mock_vaccum_cls: + mock_vaccum_cls.return_value = mock_vacuum yield mock_vacuum @@ -159,16 +164,16 @@ def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): } }, ) + yield from hass.async_block_till_done() assert "Initializing with host 127.0.0.1 (token 12345...)" in caplog.text - assert str(mock_mirobo_errors.mock_calls[-1]) == "call.Vacuum().status()" + assert mock_mirobo_errors.status.call_count == 1 assert "ERROR" in caplog.text assert "Got OSError while fetching the state" in caplog.text @asyncio.coroutine -@pytest.mark.skip(reason="Fails") -def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off): +def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_1" entity_id = "{}.{}".format(DOMAIN, entity_name) @@ -185,19 +190,20 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off): } }, ) + yield from hass.async_block_till_done() assert "Initializing with host 127.0.0.1 (token 12345...)" in caplog.text # Check state attributes state = hass.states.get(entity_id) - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 2047 + assert state.state == STATE_ERROR + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 14204 assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_ON assert state.attributes.get(ATTR_DO_NOT_DISTURB_START) == "22:00:00" assert state.attributes.get(ATTR_DO_NOT_DISTURB_END) == "06:00:00" assert state.attributes.get(ATTR_ERROR) == "Error message" - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-80" + assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-80" assert state.attributes.get(ATTR_CLEANING_TIME) == 155 assert state.attributes.get(ATTR_CLEANED_AREA) == 123 assert state.attributes.get(ATTR_FAN_SPEED) == "Quiet" @@ -215,96 +221,103 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off): assert state.attributes.get(ATTR_CLEANING_TOTAL_TIME) == 695 # Call services - yield from hass.services.async_call(DOMAIN, SERVICE_TURN_ON, blocking=True) + yield from hass.services.async_call( + DOMAIN, SERVICE_START, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.resume_or_start()], any_order=True + ) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum.start()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + yield from hass.services.async_call( + DOMAIN, SERVICE_STOP, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.stop()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().home()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + yield from hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.home()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call(DOMAIN, SERVICE_TOGGLE, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().start()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + yield from hass.services.async_call( + DOMAIN, SERVICE_LOCATE, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.find()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call(DOMAIN, SERVICE_STOP, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().stop()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_START_PAUSE, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().pause()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().home()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_LOCATE, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().find()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, {}, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().spot()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + yield from hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.spot()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() # Set speed service: yield from hass.services.async_call( - DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": 60}, blocking=True + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": 60}, + blocking=True, ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().set_fan_speed(60)], any_order=True + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.set_fan_speed(60)], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() yield from hass.services.async_call( - DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": "turbo"}, blocking=True + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": "turbo"}, + blocking=True, ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().set_fan_speed(77)], any_order=True + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.set_fan_speed(77)], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() assert "ERROR" not in caplog.text yield from hass.services.async_call( - DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": "invent"}, blocking=True + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": "invent"}, + blocking=True, ) assert "ERROR" in caplog.text yield from hass.services.async_call( - DOMAIN, SERVICE_SEND_COMMAND, {"command": "raw"}, blocking=True + DOMAIN, + SERVICE_SEND_COMMAND, + {"entity_id": entity_id, "command": "raw"}, + blocking=True, ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().raw_command("raw", None)], any_order=True + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.raw_command("raw", None)], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() yield from hass.services.async_call( DOMAIN, SERVICE_SEND_COMMAND, - {"command": "raw", "params": {"k1": 2}}, + {"entity_id": entity_id, "command": "raw", "params": {"k1": 2}}, blocking=True, ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().raw_command("raw", {"k1": 2})], any_order=True + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.raw_command("raw", {"k1": 2})], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() @asyncio.coroutine -@pytest.mark.skip(reason="Fails") def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_2" @@ -322,13 +335,14 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): } }, ) + yield from hass.async_block_till_done() assert "Initializing with host 192.168.1.100 (token 12345" in caplog.text # Check state attributes state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 2047 + assert state.state == STATE_CLEANING + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 14204 assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_OFF assert state.attributes.get(ATTR_ERROR) is None assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-30" @@ -353,47 +367,43 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): DOMAIN, SERVICE_START_REMOTE_CONTROL, {ATTR_ENTITY_ID: entity_id}, blocking=True ) - mock_mirobo_is_on.assert_has_calls( - [mock.call.Vacuum().manual_start()], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_on.assert_has_calls([mock.call.manual_start()], any_order=True) + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() control = {"duration": 1000, "rotation": -40, "velocity": -0.1} yield from hass.services.async_call( DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True ) - mock_mirobo_is_on.assert_has_calls( - [mock.call.Vacuum().manual_control(control)], any_order=True + mock_mirobo_is_on.manual_control.assert_has_calls( + [mock.call(**control)], any_order=True ) - mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() yield from hass.services.async_call( DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True ) - mock_mirobo_is_on.assert_has_calls( - [mock.call.Vacuum().manual_stop()], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_on.assert_has_calls([mock.call.manual_stop()], any_order=True) + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() control_once = {"duration": 2000, "rotation": 120, "velocity": 0.1} yield from hass.services.async_call( DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True ) - mock_mirobo_is_on.assert_has_calls( - [mock.call.Vacuum().manual_control_once(control_once)], any_order=True + mock_mirobo_is_on.manual_control_once.assert_has_calls( + [mock.call(**control_once)], any_order=True ) - mock_mirobo_is_on.assert_has_calls(status_calls, 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_on.zoned_clean.assert_has_calls( + [mock.call([[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() + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_on.reset_mock() From a39cac765e04f36908b77d459cc7691223556db8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Nov 2019 20:18:41 +0100 Subject: [PATCH 243/306] Add sensor platform to WLED integration (#28632) * Add sensor platform to WLED integration * Process review comments --- homeassistant/components/wled/__init__.py | 10 +- homeassistant/components/wled/const.py | 6 + homeassistant/components/wled/sensor.py | 141 ++++++++++++++++++++++ tests/components/wled/test_sensor.py | 61 ++++++++++ 4 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/wled/sensor.py create mode 100644 tests/components/wled/test_sensor.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 054c09eb971..cd2c091bc10 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Union from wled import WLED, WLEDConnectionError, WLEDError from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST @@ -34,6 +35,7 @@ from .const import ( ) SCAN_INTERVAL = timedelta(seconds=5) +WLED_COMPONENTS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -60,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} # Set up all platforms for this device/entry. - for component in LIGHT_DOMAIN, SWITCH_DOMAIN: + for component in WLED_COMPONENTS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -93,8 +95,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Unload entities for this entry/device. await asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN), - hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), + *( + hass.config_entries.async_forward_entry_unload(entry, component) + for component in WLED_COMPONENTS + ) ) # Cleanup diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 0836c801632..5fc24d74d63 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -14,7 +14,9 @@ ATTR_DURATION = "duration" ATTR_FADE = "fade" ATTR_IDENTIFIERS = "identifiers" ATTR_INTENSITY = "intensity" +ATTR_LED_COUNT = "led_count" ATTR_MANUFACTURER = "manufacturer" +ATTR_MAX_POWER = "max_power" ATTR_MODEL = "model" ATTR_ON = "on" ATTR_PALETTE = "palette" @@ -25,3 +27,7 @@ ATTR_SOFTWARE_VERSION = "sw_version" ATTR_SPEED = "speed" ATTR_TARGET_BRIGHTNESS = "target_brightness" ATTR_UDP_PORT = "udp_port" + +# Units of measurement +CURRENT_MA = "mA" +DATA_BYTES = "bytes" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py new file mode 100644 index 00000000000..f464b27e140 --- /dev/null +++ b/homeassistant/components/wled/sensor.py @@ -0,0 +1,141 @@ +"""Support for WLED sensors.""" +from datetime import timedelta +import logging +from typing import Callable, List, Optional, Union + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.dt import utcnow + +from . import WLED, WLEDDeviceEntity +from .const import ( + ATTR_LED_COUNT, + ATTR_MAX_POWER, + CURRENT_MA, + DATA_BYTES, + DATA_WLED_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up WLED sensor based on a config entry.""" + wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + + sensors = [ + WLEDEstimatedCurrentSensor(entry.entry_id, wled), + WLEDUptimeSensor(entry.entry_id, wled), + WLEDFreeHeapSensor(entry.entry_id, wled), + ] + + async_add_entities(sensors, True) + + +class WLEDSensor(WLEDDeviceEntity): + """Defines a WLED sensor.""" + + def __init__( + self, + entry_id: str, + wled: WLED, + name: str, + icon: str, + unit_of_measurement: str, + key: str, + ) -> None: + """Initialize WLED sensor.""" + self._state = None + self._unit_of_measurement = unit_of_measurement + self._key = key + + super().__init__(entry_id, wled, name, icon) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.wled.device.info.mac_address}_{self._key}" + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class WLEDEstimatedCurrentSensor(WLEDSensor): + """Defines a WLED estimated current sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED estimated current sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Estimated Current", + "mdi:power", + CURRENT_MA, + "estimated_current", + ) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.info.leds.power + self._attributes = { + ATTR_LED_COUNT: self.wled.device.info.leds.count, + ATTR_MAX_POWER: self.wled.device.info.leds.max_power, + } + + +class WLEDUptimeSensor(WLEDSensor): + """Defines a WLED uptime sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED uptime sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Uptime", + "mdi:clock-outline", + None, + "uptime", + ) + + @property + def device_class(self) -> Optional[str]: + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP + + async def _wled_update(self) -> None: + """Update WLED uptime sensor.""" + uptime = utcnow() - timedelta(seconds=self.wled.device.info.uptime) + self._state = uptime.replace(microsecond=0).isoformat() + + +class WLEDFreeHeapSensor(WLEDSensor): + """Defines a WLED free heap sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED free heap sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Free Memory", + "mdi:memory", + DATA_BYTES, + "free_heap", + ) + + async def _wled_update(self) -> None: + """Update WLED uptime sensor.""" + self._state = self.wled.device.info.free_heap diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py new file mode 100644 index 00000000000..a1247a8c373 --- /dev/null +++ b/tests/components/wled/test_sensor.py @@ -0,0 +1,61 @@ +"""Tests for the WLED sensor platform.""" +from datetime import datetime + +from asynctest import patch + +from homeassistant.components.wled.const import ( + ATTR_LED_COUNT, + ATTR_MAX_POWER, + CURRENT_MA, + DATA_BYTES, +) +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the WLED sensors.""" + + test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) + with patch("homeassistant.components.wled.sensor.utcnow", return_value=test_time): + await init_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.wled_rgb_light_estimated_current") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:power" + assert state.attributes.get(ATTR_LED_COUNT) == 30 + assert state.attributes.get(ATTR_MAX_POWER) == 850 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENT_MA + assert state.state == "470" + + entry = entity_registry.async_get("sensor.wled_rgb_light_estimated_current") + assert entry + assert entry.unique_id == "aabbccddeeff_estimated_current" + + state = hass.states.get("sensor.wled_rgb_light_uptime") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.state == "2019-11-11T09:10:00+00:00" + + entry = entity_registry.async_get("sensor.wled_rgb_light_uptime") + assert entry + assert entry.unique_id == "aabbccddeeff_uptime" + + state = hass.states.get("sensor.wled_rgb_light_free_memory") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:memory" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_BYTES + assert state.state == "14600" + + entry = entity_registry.async_get("sensor.wled_rgb_light_free_memory") + assert entry + assert entry.unique_id == "aabbccddeeff_free_heap" From 179a2eb187882098b42051ad2b7a5ace779ae730 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Nov 2019 15:41:02 -0700 Subject: [PATCH 244/306] Create base entity for SimpliSafe --- .../components/simplisafe/__init__.py | 76 ++++++++++++++++++- .../simplisafe/alarm_control_panel.py | 62 ++------------- .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 85 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 0c0595896ed..2514c5e9ed9 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -22,7 +22,11 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.dispatcher import async_dispatcher_send +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.service import verify_domain_control @@ -240,3 +244,73 @@ class SimpliSafe: tasks = [self._update_system(system) for system in self.systems.values()] await asyncio.gather(*tasks) + + +class SimpliSafeEntity(Entity): + """Define a base SimpliSafe entity.""" + + def __init__(self, system, name, *, serial=None): + """Initialize.""" + self._async_unsub_dispatcher_connect = None + self._attrs = {ATTR_SYSTEM_ID: system.system_id} + self._name = name + self._online = True + self._system = system + + if serial: + self._serial = serial + else: + self._serial = system.serial + + @property + def available(self): + """Return whether the entity is available.""" + # We can easily detect if the V3 system is offline, but no simple check exists + # for the V2 system. Therefore, we mark the entity as available if: + # 1. We can verify that the system is online (assuming True if we can't) + # 2. We can verify that the entity is online + system_offline = self._system.version == 3 and self._system.offline + return not system_offline and self._online + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._system.system_id)}, + "manufacturer": "SimpliSafe", + "model": self._system.version, + "name": self._name, + "via_device": (DOMAIN, self._system.serial), + } + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._system.address} {self._name}" + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return self._serial + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index d44a1c7760a..bbd9a5d4e51 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -2,7 +2,7 @@ import logging import re -from simplipy.sensor import SensorTypes +from simplipy.entity import EntityTypes from simplipy.system import SystemStates from homeassistant.components.alarm_control_panel import ( @@ -16,11 +16,10 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import utc_from_timestamp -from .const import DATA_CLIENT, DOMAIN, TOPIC_UPDATE +from . import SimpliSafeEntity +from .const import DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -33,7 +32,6 @@ ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" ATTR_LAST_EVENT_TYPE = "last_event_type" ATTR_RF_JAMMING = "rf_jamming" -ATTR_SYSTEM_ID = "system_id" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" @@ -55,18 +53,16 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class SimpliSafeAlarm(AlarmControlPanel): +class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): """Representation of a SimpliSafe alarm.""" def __init__(self, simplisafe, system, code): """Initialize the SimpliSafe alarm.""" - self._async_unsub_dispatcher_connect = None - self._attrs = {ATTR_SYSTEM_ID: system.system_id} + super().__init__(system, "Alarm Control Panel") self._changed_by = None self._code = code self._simplisafe = simplisafe self._state = None - self._system = system # Some properties only exist for V2 or V3 systems: for prop in ( @@ -93,39 +89,11 @@ class SimpliSafeAlarm(AlarmControlPanel): return FORMAT_NUMBER return FORMAT_TEXT - @property - def device_info(self): - """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self._system.system_id)}, - "manufacturer": "SimpliSafe", - "model": self._system.version, - # The name should become more dynamic once we deduce a way to - # get various other sensors from SimpliSafe in a reliable manner: - "name": "Keypad", - "via_device": (DOMAIN, self._system.serial), - } - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attrs - - @property - def name(self): - """Return the name of the entity.""" - return self._system.address - @property def state(self): """Return the state of the entity.""" return self._state - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._system.system_id - def _validate_code(self, code, state): """Validate given code.""" check = self._code is None or code == self._code @@ -133,18 +101,6 @@ class SimpliSafeAlarm(AlarmControlPanel): _LOGGER.warning("Wrong code entered for %s", state) return check - async def async_added_to_hass(self): - """Register callbacks.""" - - @callback - def update(): - """Update the state.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update - ) - async def async_alarm_disarm(self, code=None): """Send disarm command.""" if not self._validate_code(code, "disarming"): @@ -174,6 +130,7 @@ class SimpliSafeAlarm(AlarmControlPanel): self._changed_by = event_data["pinName"] if self._system.state == SystemStates.error: + self._online = False return if self._system.state == SystemStates.off: @@ -195,15 +152,10 @@ class SimpliSafeAlarm(AlarmControlPanel): ATTR_ALARM_ACTIVE: self._system.alarm_going_off, ATTR_LAST_EVENT_INFO: last_event["info"], ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], - ATTR_LAST_EVENT_SENSOR_TYPE: SensorTypes(last_event["sensorType"]).name, + ATTR_LAST_EVENT_SENSOR_TYPE: EntityTypes(last_event["sensorType"]).name, ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp( last_event["eventTimestamp"] ), ATTR_LAST_EVENT_TYPE: last_event["eventType"], } ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect dispatcher listener when removed.""" - if self._async_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 96b337def55..254e947ed25 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.0.1" + "simplisafe-python==5.1.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 6f0b40230ed..ccf732ce2ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1769,7 +1769,7 @@ shodan==1.19.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.0.1 +simplisafe-python==5.1.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 691e8d413d5..99b9c744041 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -546,7 +546,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.0.1 +simplisafe-python==5.1.0 # homeassistant.components.sleepiq sleepyq==0.7 From 25f0b709663c21dbf7ad80670a35cbcaa8baec30 Mon Sep 17 00:00:00 2001 From: Gerard Date: Sat, 9 Nov 2019 23:43:04 +0100 Subject: [PATCH 245/306] Upgrade bimmer_connected to 0.6.2 (#28651) * Upgrade bimmer_connected to 0.6.1 * Remove time.sleep from library --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 29366715d23..a88675ef80f 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -3,7 +3,7 @@ "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "requirements": [ - "bimmer_connected==0.6.0" + "bimmer_connected==0.6.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ccf732ce2ae..ee331d1a320 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ beewi_smartclim==0.0.7 bellows-homeassistant==0.10.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.6.0 +bimmer_connected==0.6.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 From ef687a36ff36de5a115a6be014909469ac372793 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 9 Nov 2019 17:02:56 -0600 Subject: [PATCH 246/306] Add Plex debug logging (#28665) --- homeassistant/components/plex/config_flow.py | 2 +- homeassistant/components/plex/media_player.py | 3 +++ homeassistant/components/plex/sensor.py | 1 + homeassistant/components/plex/server.py | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index c03b958b2da..cb79c08b16e 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -102,7 +102,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_select_server() except Exception as error: # pylint: disable=broad-except - _LOGGER.error("Unknown error connecting to Plex server: %s", error) + _LOGGER.exception("Unknown error connecting to Plex server: %s", error) return self.async_abort(reason="unknown") if errors: diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4c32c1e6376..1fe83928d29 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -75,6 +75,7 @@ def _async_add_entities( hass, registry, config_entry, async_add_entities, server_id, new_entities ): """Set up Plex media_player entities.""" + _LOGGER.debug("New entities: %s", new_entities) entities = [] plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] for entity_params in new_entities: @@ -142,6 +143,7 @@ class PlexMediaPlayer(MediaPlayerDevice): """Run when about to be added to hass.""" server_id = self.plex_server.machine_identifier + _LOGGER.debug("Added %s [%s]", self.entity_id, self.unique_id) unsub = async_dispatcher_connect( self.hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), @@ -152,6 +154,7 @@ class PlexMediaPlayer(MediaPlayerDevice): @callback def async_refresh_media_player(self, device, session): """Set instance objects and trigger an entity state update.""" + _LOGGER.debug("Refreshing %s [%s / %s]", self.entity_id, device, session) self.device = device self.session = session self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 287f0edf39a..2a994b08a7b 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -93,6 +93,7 @@ class PlexSensor(Entity): def update(self): """Update method for Plex sensor.""" + _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) now_playing = [] for sess in self.sessions: user = sess.usernames[0] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 28380e714ac..5df55589bb4 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -95,6 +95,7 @@ class PlexServer: def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" + _LOGGER.debug("Refreshing %s", unique_id) dispatcher_send( self._hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), From 65bf0a30f0668587b87c41a01e764360335459c2 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 10 Nov 2019 00:32:12 +0000 Subject: [PATCH 247/306] [ci skip] Translation update --- .../components/climate/.translations/ca.json | 8 ++++++++ homeassistant/components/fan/.translations/ca.json | 12 ++++++++++++ homeassistant/components/lock/.translations/ca.json | 4 ++++ .../components/vacuum/.translations/ca.json | 3 +++ 4 files changed, 27 insertions(+) create mode 100644 homeassistant/components/fan/.translations/ca.json diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json index 480d90310d9..7aff7383a39 100644 --- a/homeassistant/components/climate/.translations/ca.json +++ b/homeassistant/components/climate/.translations/ca.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "set_hvac_mode": "Canvia el mode HVAC de {entity_name}" + }, "condtion_type": { "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", "is_preset_mode": "{entity_name} est\u00e0 configurat/ada en un mode preestablert espec\u00edfic" + }, + "trigger_type": { + "current_humidity_changed": "Ha canviat la humitat mesurada per {entity_name}", + "current_temperature_changed": "Ha canviat la temperatura mesurada per {entity_name}", + "hvac_mode_changed": "El mode HVAC de {entity_name} ha canviat" } } } \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ca.json b/homeassistant/components/fan/.translations/ca.json new file mode 100644 index 00000000000..1002f8d7dd6 --- /dev/null +++ b/homeassistant/components/fan/.translations/ca.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Apaga {entity_name}", + "turn_on": "Enc\u00e9n {entity_name}" + }, + "trigger_type": { + "turned_off": "{entity_name} s'ha apagat", + "turned_on": "{entity_name} s'ha enc\u00e8s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ca.json b/homeassistant/components/lock/.translations/ca.json index 53198a21573..69655ac1daa 100644 --- a/homeassistant/components/lock/.translations/ca.json +++ b/homeassistant/components/lock/.translations/ca.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} est\u00e0 bloquejat/ada", "is_unlocked": "{entity_name} est\u00e0 desbloquejat/ada" + }, + "trigger_type": { + "locked": "{entity_name} s'ha bloquejat", + "unlocked": "{entity_name} s'ha desbloquejat" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json index c004120a8c7..df2ea439e0e 100644 --- a/homeassistant/components/vacuum/.translations/ca.json +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -2,6 +2,9 @@ "device_automation": { "condtion_type": { "is_cleaning": "{entity_name} est\u00e0 netejant" + }, + "trigger_type": { + "cleaning": "{entity_name} ha comen\u00e7at a netejar" } } } \ No newline at end of file From a9536e4ed1c9831982e23525931329c588dafa31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sun, 10 Nov 2019 08:25:10 +0100 Subject: [PATCH 248/306] verisure autolock service (#27366) --- homeassistant/components/verisure/__init__.py | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index f4313c7c1ac..d5cc7f31efb 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -2,9 +2,9 @@ import logging import threading from datetime import timedelta - from jsonpath import jsonpath import verisure + import voluptuous as vol from homeassistant.const import ( @@ -39,6 +39,8 @@ MIN_SCAN_INTERVAL = timedelta(minutes=1) DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) SERVICE_CAPTURE_SMARTCAM = "capture_smartcam" +SERVICE_DISABLE_AUTOLOCK = "disable_autolock" +SERVICE_ENABLE_AUTOLOCK = "enable_autolock" HUB = None @@ -68,7 +70,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -CAPTURE_IMAGE_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string}) +DEVICE_SERIAL_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string}) def setup(hass, config): @@ -93,16 +95,44 @@ def setup(hass, config): ): discovery.load_platform(hass, component, DOMAIN, {}, config) - def capture_smartcam(service): + async def capture_smartcam(service): """Capture a new picture from a smartcam.""" - device_id = service.data.get(ATTR_DEVICE_SERIAL) - HUB.smartcam_capture(device_id) - _LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) + device_id = service.data[ATTR_DEVICE_SERIAL] + try: + await hass.async_add_executor_job(HUB.smartcam_capture, device_id) + _LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) + except verisure.Error as ex: + _LOGGER.error("Could not capture image, %s", ex) hass.services.register( - DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=CAPTURE_IMAGE_SCHEMA + DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=DEVICE_SERIAL_SCHEMA ) + async def disable_autolock(service): + """Disable autolock on a doorlock.""" + device_id = service.data[ATTR_DEVICE_SERIAL] + try: + await hass.async_add_executor_job(HUB.disable_autolock, device_id) + _LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL) + except verisure.Error as ex: + _LOGGER.error("Could not disable autolock, %s", ex) + + hass.services.register( + DOMAIN, SERVICE_DISABLE_AUTOLOCK, disable_autolock, schema=DEVICE_SERIAL_SCHEMA + ) + + async def enable_autolock(service): + """Enable autolock on a doorlock.""" + device_id = service.data[ATTR_DEVICE_SERIAL] + try: + await hass.async_add_executor_job(HUB.enable_autolock, device_id) + _LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL) + except verisure.Error as ex: + _LOGGER.error("Could not enable autolock, %s", ex) + + hass.services.register( + DOMAIN, SERVICE_ENABLE_AUTOLOCK, enable_autolock, schema=DEVICE_SERIAL_SCHEMA + ) return True @@ -175,6 +205,14 @@ class VerisureHub: """Capture a new image from a smartcam.""" self.session.capture_image(device_id) + def disable_autolock(self, device_id): + """Disable autolock.""" + self.session.set_lock_config(device_id, auto_lock_enabled=False) + + def enable_autolock(self, device_id): + """Enable autolock.""" + self.session.set_lock_config(device_id, auto_lock_enabled=True) + def get(self, jpath, *args): """Get values from the overview that matches the jsonpath.""" res = jsonpath(self.overview, jpath % args) From 206547a5d8abbcf7bc0463d312cc9f68cdcfce07 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 10 Nov 2019 04:35:12 -0600 Subject: [PATCH 249/306] Skip updating idle Plex clients (#28664) * Skip updating idle clients * Different operators --- homeassistant/components/plex/server.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 5df55589bb4..69838fbf27f 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -44,6 +44,7 @@ class PlexServer: self._hass = hass self._plex_server = None self._known_clients = set() + self._known_idle = set() self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) @@ -123,6 +124,7 @@ class PlexServer: return for device in devices: + self._known_idle.discard(device.machineIdentifier) available_clients[device.machineIdentifier] = {"device": device} if device.machineIdentifier not in self._known_clients: @@ -131,6 +133,7 @@ class PlexServer: for session in sessions: for player in session.players: + self._known_idle.discard(player.machineIdentifier) available_clients.setdefault( player.machineIdentifier, {"device": player} ) @@ -151,9 +154,12 @@ class PlexServer: self._known_clients.update(new_clients) - idle_clients = self._known_clients.difference(available_clients) + idle_clients = (self._known_clients - self._known_idle).difference( + available_clients + ) for client_id in idle_clients: self.refresh_entity(client_id, None, None) + self._known_idle.add(client_id) if new_entity_configs: dispatcher_send( From 66a574eca436872047614e26a7dd21cef3cc9c94 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 10 Nov 2019 20:37:34 +0100 Subject: [PATCH 250/306] Hue: store current sensor entities by bridge (#28679) --- homeassistant/components/hue/sensor_base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 62bd98df3a2..bf64fed0c95 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -15,7 +15,7 @@ from homeassistant.util.dt import utcnow from .helpers import remove_devices -CURRENT_SENSORS = "current_sensors" +CURRENT_SENSORS_FORMAT = "{}_current_sensors" SENSOR_MANAGER_FORMAT = "{}_sensor_manager" _LOGGER = logging.getLogger(__name__) @@ -31,8 +31,9 @@ def _device_id(aiohue_sensor): async def async_setup_entry(hass, config_entry, async_add_entities, binary=False): """Set up the Hue sensors from a config entry.""" + sensor_key = CURRENT_SENSORS_FORMAT.format(config_entry.data["host"]) bridge = hass.data[hue.DOMAIN][config_entry.data["host"]] - hass.data[hue.DOMAIN].setdefault(CURRENT_SENSORS, {}) + hass.data[hue.DOMAIN].setdefault(sensor_key, {}) sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data["host"]) manager = hass.data[hue.DOMAIN].get(sm_key) @@ -127,7 +128,8 @@ class SensorManager: new_sensors = [] new_binary_sensors = [] primary_sensor_devices = {} - current = self.hass.data[hue.DOMAIN][CURRENT_SENSORS] + sensor_key = CURRENT_SENSORS_FORMAT.format(self.config_entry.data["host"]) + current = self.hass.data[hue.DOMAIN][sensor_key] # Physical Hue motion sensors present as three sensors in the API: a # presence sensor, a temperature sensor, and a light level sensor. Of From 65dd7d998be38bd4d8acbbb78e07f4a518677d13 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 10 Nov 2019 13:51:00 -0800 Subject: [PATCH 251/306] #28645: Bump up zm-py to 0.4.0 (#28681) --- homeassistant/components/zoneminder/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index c29a97e857e..5e834e2fa4f 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -3,7 +3,7 @@ "name": "Zoneminder", "documentation": "https://www.home-assistant.io/integrations/zoneminder", "requirements": [ - "zm-py==0.3.3" + "zm-py==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ee331d1a320..997a218f5e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2075,4 +2075,4 @@ zigpy-xbee-homeassistant==0.6.0 zigpy-zigate==0.5.0 # homeassistant.components.zoneminder -zm-py==0.3.3 +zm-py==0.4.0 From 0ac8b297cd89aa6fe444974f1fd17af31bbbdc7e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 11 Nov 2019 00:32:13 +0000 Subject: [PATCH 252/306] [ci skip] Translation update --- homeassistant/components/cover/.translations/ru.json | 1 - homeassistant/components/huawei_lte/.translations/ru.json | 3 ++- homeassistant/components/ifttt/.translations/fr.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 47608a1ff77..043e5a42d2a 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -9,7 +9,6 @@ "is_tilt_position": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" }, "trigger_type": { - "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index e64018b2a3c..ec28325dcdd 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE" }, "error": { "connection_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", diff --git a/homeassistant/components/ifttt/.translations/fr.json b/homeassistant/components/ifttt/.translations/fr.json index d083a624d70..659b11ae98a 100644 --- a/homeassistant/components/ifttt/.translations/fr.json +++ b/homeassistant/components/ifttt/.translations/fr.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Une seule instance est n\u00e9cessaire." }, "create_entry": { - "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez utiliser l'action \"Effectuer une demande Web\" \u00e0 partir de [l'applet IFTTT Webhook] ( {applet_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez utiliser l'action \"Effectuer une demande Web\" \u00e0 partir de [l'applet IFTTT Webhook]({applet_url}). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation]({docs_url}) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." }, "step": { "user": { From aea7c1c0ceeec237d4463cf29a6ac0e70c7fbd6f Mon Sep 17 00:00:00 2001 From: Jon Gilmore <7232986+JonGilmore@users.noreply.github.com> Date: Sun, 10 Nov 2019 18:51:24 -0600 Subject: [PATCH 253/306] Add codeowner for lutron integration (#28682) * add codeowner for lutron integration * ran hassfest --- CODEOWNERS | 1 + homeassistant/components/lutron/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index cb3d0817d59..879a1c8f55d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -177,6 +177,7 @@ homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff +homeassistant/components/lutron/* @JonGilmore homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mcp23017/* @jardiamj diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index cace2770de0..f8439e3c1ec 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -6,5 +6,7 @@ "pylutron==0.2.5" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@JonGilmore" + ] } From b6c79764774389e87df053868cdf6b080d6c6426 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 11 Nov 2019 07:59:07 +0100 Subject: [PATCH 254/306] Add xiaomi_miio chuangmi.plug.hmi206 (#28688) Related: https://github.com/rytilahti/python-miio/issues/574 --- homeassistant/components/xiaomi_miio/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 42586cd5970..023243a1995 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -48,6 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( "chuangmi.plug.v2", "chuangmi.plug.v3", "chuangmi.plug.hmi205", + "chuangmi.plug.hmi206", "lumi.acpartner.v3", ] ), @@ -158,6 +159,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "chuangmi.plug.m3", "chuangmi.plug.v2", "chuangmi.plug.hmi205", + "chuangmi.plug.hmi206", ]: plug = ChuangmiPlug(host, token, model=model) device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) From 7bfde2dd33b4bc4d5464f45729852a7bd1878b01 Mon Sep 17 00:00:00 2001 From: Kevin Lee Date: Mon, 11 Nov 2019 02:07:48 -0500 Subject: [PATCH 255/306] Add Lutron hybrid keypad raise/lower buttons (#28674) * Lutron: Add support for hybrid keypad raise/lower buttons * Add MasterRaiseLower --- homeassistant/components/lutron/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 09ab0fc747b..ac9a4eab417 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -72,6 +72,8 @@ def setup(hass, base_config): if button.name != "Unknown Button" and button.button_type in ( "SingleAction", "Toggle", + "SingleSceneRaiseLower", + "MasterRaiseLower", ): # Associate an LED with a button if there is one led = next( From 90e723e25eb8fc559c7bb67a0cec2f7e1983a5cb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 11 Nov 2019 11:53:57 +0100 Subject: [PATCH 256/306] Allow icons to be masked (#28692) --- homeassistant/components/frontend/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f82350d994d..7ef2bd38644 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -52,6 +52,7 @@ MANIFEST_JSON = { "src": "/static/icons/favicon-{size}x{size}.png".format(size=size), "sizes": "{size}x{size}".format(size=size), "type": "image/png", + "purpose": "maskable any", } for size in (192, 384, 512, 1024) ], From 3ce850234f826acf466e0b631a8f7ce4cfa2e152 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 11 Nov 2019 13:51:26 +0100 Subject: [PATCH 257/306] fix typo in comments (#28694) The global is called `PARALLEL_UPDATES` not `PARALLEL_UPDATE`. --- homeassistant/helpers/entity_platform.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5c59dc6c13e..376a6e23e9a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -272,13 +272,13 @@ class EntityPlatform: entity.platform = self # Async entity - # PARALLEL_UPDATE == None: entity.parallel_updates = None - # PARALLEL_UPDATE == 0: entity.parallel_updates = None - # PARALLEL_UPDATE > 0: entity.parallel_updates = Semaphore(p) + # PARALLEL_UPDATES == None: entity.parallel_updates = None + # PARALLEL_UPDATES == 0: entity.parallel_updates = None + # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p) # Sync entity - # PARALLEL_UPDATE == None: entity.parallel_updates = Semaphore(1) - # PARALLEL_UPDATE == 0: entity.parallel_updates = None - # PARALLEL_UPDATE > 0: entity.parallel_updates = Semaphore(p) + # PARALLEL_UPDATES == None: entity.parallel_updates = Semaphore(1) + # PARALLEL_UPDATES == 0: entity.parallel_updates = None + # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p) if hasattr(entity, "async_update") and not self.parallel_updates: entity.parallel_updates = None elif not hasattr(entity, "async_update") and self.parallel_updates == 0: From cfcacc28279f496e5408b343b560208e632db57d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 11 Nov 2019 12:26:56 -0600 Subject: [PATCH 258/306] Bump plexwebsocket to 0.0.5 (#28703) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index b7179174ea4..eaf3028b39f 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.0.6", "plexauth==0.0.5", - "plexwebsocket==0.0.4" + "plexwebsocket==0.0.5" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index 997a218f5e2..01538784fec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -986,7 +986,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.4 +plexwebsocket==0.0.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99b9c744041..d352e21b029 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.4 +plexwebsocket==0.0.5 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From decab3e15b2ef80e9e9ed3ea3bf20717a8811e51 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 11 Nov 2019 21:30:00 +0100 Subject: [PATCH 259/306] Add config flow tests for OwnTracks (#28644) * Add config flow tests for OwnTracks * Fix pylint * Woops, uncomment test * Woops again, logs removed * Review from @MartinHjelmare + fix pylint --- .../components/owntracks/__init__.py | 2 +- .../components/owntracks/config_flow.py | 19 ++- homeassistant/components/owntracks/const.py | 3 + .../components/owntracks/messages.py | 8 +- .../components/owntracks/test_config_flow.py | 128 ++++++++++++++++-- 5 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/owntracks/const.py diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 7e65ff3d51d..d30e667f368 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -14,12 +14,12 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup +from .const import DOMAIN from .config_flow import CONF_SECRET from .messages import async_handle_message _LOGGER = logging.getLogger(__name__) -DOMAIN = "owntracks" CONF_MAX_GPS_ACCURACY = "max_gps_accuracy" CONF_WAYPOINT_IMPORT = "waypoints" CONF_WAYPOINT_WHITELIST = "waypoint_whitelist" diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 343a6d90b52..a59cd869c74 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -3,6 +3,8 @@ from homeassistant import config_entries from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.auth.util import generate_secret +from .const import DOMAIN # noqa pylint: disable=unused-import + CONF_SECRET = "secret" CONF_CLOUDHOOK = "cloudhook" @@ -17,8 +19,7 @@ def supports_encryption(): return False -@config_entries.HANDLERS.register("owntracks") -class OwnTracksFlow(config_entries.ConfigFlow): +class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): """Set up OwnTracks.""" VERSION = 1 @@ -36,14 +37,9 @@ class OwnTracksFlow(config_entries.ConfigFlow): secret = generate_secret(16) if supports_encryption(): - secret_desc = ( - "The encryption key is {} " - "(on Android under preferences -> advanced)".format(secret) - ) + secret_desc = f"The encryption key is {secret} (on Android under preferences -> advanced)" else: - secret_desc = ( - "Encryption is not supported because libsodium is not " "installed." - ) + secret_desc = "Encryption is not supported because nacl is not installed." return self.async_create_entry( title="OwnTracks", @@ -55,8 +51,7 @@ class OwnTracksFlow(config_entries.ConfigFlow): description_placeholders={ "secret": secret_desc, "webhook_url": webhook_url, - "android_url": "https://play.google.com/store/apps/details?" - "id=org.owntracks.android", + "android_url": "https://play.google.com/store/apps/details?id=org.owntracks.android", "ios_url": "https://itunes.apple.com/us/app/owntracks/id692424691?mt=8", "docs_url": "https://www.home-assistant.io/integrations/owntracks/", }, @@ -64,6 +59,8 @@ class OwnTracksFlow(config_entries.ConfigFlow): async def async_step_import(self, user_input): """Import a config flow from configuration.""" + if self._async_current_entries(): + return self.async_abort(reason="one_instance_allowed") webhook_id, _webhook_url, cloudhook = await self._get_webhook_id() secret = generate_secret(16) return self.async_create_entry( diff --git a/homeassistant/components/owntracks/const.py b/homeassistant/components/owntracks/const.py new file mode 100644 index 00000000000..c7caa201ca3 --- /dev/null +++ b/homeassistant/components/owntracks/const.py @@ -0,0 +1,3 @@ +"""Constants for OwnTracks.""" + +DOMAIN = "owntracks" diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 465d2762f74..0cb65c774b5 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -107,7 +107,7 @@ def _decrypt_payload(secret, topic, ciphertext): try: keylen, decrypt = get_cipher() except OSError: - _LOGGER.warning("Ignoring encrypted payload because libsodium not installed") + _LOGGER.warning("Ignoring encrypted payload because nacl not installed") return None if isinstance(secret, dict): @@ -117,8 +117,7 @@ def _decrypt_payload(secret, topic, ciphertext): if key is None: _LOGGER.warning( - "Ignoring encrypted payload because no decryption key known " - "for topic %s", + "Ignoring encrypted payload because no decryption key known for topic %s", topic, ) return None @@ -134,8 +133,7 @@ def _decrypt_payload(secret, topic, ciphertext): return message except ValueError: _LOGGER.warning( - "Ignoring encrypted payload because unable to decrypt using " - "key for topic %s", + "Ignoring encrypted payload because unable to decrypt using key for topic %s", topic, ) return None diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index c4e2a54f69a..54e33a1ab61 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,25 +1,135 @@ """Tests for OwnTracks config flow.""" -from unittest.mock import patch +from unittest.mock import Mock, patch +import pytest +from homeassistant import data_entry_flow +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.components.owntracks import config_flow +from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET +from homeassistant.components.owntracks.const import DOMAIN from homeassistant.setup import async_setup_component -from tests.common import mock_coro + +from tests.common import mock_coro, MockConfigEntry -async def test_config_flow_import(hass): +CONF_WEBHOOK_URL = "webhook_url" + +BASE_URL = "http://example.com" +CLOUDHOOK = False +SECRET = "secret" +WEBHOOK_ID = "webhook_id" +WEBHOOK_URL = f"{BASE_URL}/api/webhook/webhook_id" + + +@pytest.fixture(name="webhook_id") +def mock_webhook_id(): + """Mock webhook_id.""" + with patch( + "homeassistant.components.webhook.async_generate_id", return_value=WEBHOOK_ID + ): + yield + + +@pytest.fixture(name="secret") +def mock_secret(): + """Mock secret.""" + with patch("binascii.hexlify", return_value=str.encode(SECRET)): + yield + + +@pytest.fixture(name="not_supports_encryption") +def mock_not_supports_encryption(): + """Mock non successful nacl import.""" + with patch( + "homeassistant.components.owntracks.config_flow.supports_encryption", + return_value=False, + ): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + hass.config.api = Mock(base_url=BASE_URL) + flow = config_flow.OwnTracksFlow() + flow.hass = hass + return flow + + +async def test_user(hass, webhook_id, secret): + """Test user step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "OwnTracks" + assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID + assert result["data"][CONF_SECRET] == SECRET + assert result["data"][CONF_CLOUDHOOK] == CLOUDHOOK + assert result["description_placeholders"][CONF_WEBHOOK_URL] == WEBHOOK_URL + + +async def test_import(hass, webhook_id, secret): + """Test import step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "OwnTracks" + assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID + assert result["data"][CONF_SECRET] == SECRET + assert result["data"][CONF_CLOUDHOOK] == CLOUDHOOK + assert result["description_placeholders"] is None + + +async def test_import_setup(hass): """Test that we automatically create a config flow.""" - assert not hass.config_entries.async_entries("owntracks") - assert await async_setup_component(hass, "owntracks", {"owntracks": {}}) + assert not hass.config_entries.async_entries(DOMAIN) + assert await async_setup_component(hass, DOMAIN, {"owntracks": {}}) await hass.async_block_till_done() - assert hass.config_entries.async_entries("owntracks") + assert hass.config_entries.async_entries(DOMAIN) -async def test_config_flow_unload(hass): +async def test_abort_if_already_setup(hass): + """Test that we can't add more than one instance.""" + flow = init_config_flow(hass) + + MockConfigEntry(domain=DOMAIN, data={}).add_to_hass(hass) + assert hass.config_entries.async_entries(DOMAIN) + + # Should fail, already setup (import) + result = await flow.async_step_import({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_allowed" + + # Should fail, already setup (flow) + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_allowed" + + +async def test_user_not_supports_encryption(hass, not_supports_encryption): + """Test user step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + result["description_placeholders"]["secret"] + == "Encryption is not supported because nacl is not installed." + ) + + +async def test_unload(hass): """Test unloading a config flow.""" with patch( "homeassistant.config_entries.ConfigEntries" ".async_forward_entry_setup" ) as mock_forward: result = await hass.config_entries.flow.async_init( - "owntracks", context={"source": "import"}, data={} + DOMAIN, context={"source": "import"}, data={} ) assert len(mock_forward.mock_calls) == 1 @@ -51,7 +161,7 @@ async def test_with_cloud_sub(hass): return_value=mock_coro("https://hooks.nabu.casa/ABCD"), ): result = await hass.config_entries.flow.async_init( - "owntracks", context={"source": "user"}, data={} + DOMAIN, context={"source": "user"}, data={} ) entry = result["result"] From a9a1c2b91dd461ab462a1a6b7af6d66a4c01748e Mon Sep 17 00:00:00 2001 From: GaryOkie <37629938+GaryOkie@users.noreply.github.com> Date: Mon, 11 Nov 2019 17:35:09 -0600 Subject: [PATCH 260/306] Update Homekit climate.py to remap current mode (#28625) * Update Homekit climate.py to remap current mode This update changes the mapping of Homekit's Current Mode Heating/Cooling State to show the HASS Hvac_action attribute as "idle" instead of "off" when the returned value is 0. * Update climate.py removed imported constant no longer being used (CURRENT_HVAC_OFF) * corrected update to climate.py trying again to remove unused constant. * Update test_climate.py * removed "change" comment The added comment describing the change was not needed and should not be included, as it will already be described via "git annotate" (per @jc2k) --- homeassistant/components/homekit_controller/climate.py | 4 ++-- tests/components/homekit_controller/test_climate.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 194a2b5a42e..1f9118ff838 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -13,7 +13,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - CURRENT_HVAC_OFF, + CURRENT_HVAC_IDLE, CURRENT_HVAC_HEAT, CURRENT_HVAC_COOL, SUPPORT_TARGET_TEMPERATURE, @@ -39,7 +39,7 @@ MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS) CURRENT_MODE_HOMEKIT_TO_HASS = { - 0: CURRENT_HVAC_OFF, + 0: CURRENT_HVAC_IDLE, 1: CURRENT_HVAC_HEAT, 2: CURRENT_HVAC_COOL, } diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 72c2038f1fe..0d3544a6f55 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -213,7 +213,7 @@ async def test_hvac_mode_vs_hvac_action(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "heat" - assert state.attributes["hvac_action"] == "off" + assert state.attributes["hvac_action"] == "idle" # Simulate that current temperature is below target temp # Heating might be on and hvac_action currently 'heat' From 25b06312647947c6092b97fd42a85c218f6c7ef6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 12 Nov 2019 00:32:11 +0000 Subject: [PATCH 261/306] [ci skip] Translation update --- .../components/almond/.translations/fr.json | 3 ++- .../components/almond/.translations/it.json | 3 ++- .../components/almond/.translations/sl.json | 3 ++- .../components/climate/.translations/fr.json | 12 +++++++++ .../components/climate/.translations/sl.json | 17 ++++++++++++ .../components/deconz/.translations/it.json | 6 ++++- .../components/deconz/.translations/sl.json | 18 ++++++++++++- .../components/fan/.translations/fr.json | 16 ++++++++++++ .../components/fan/.translations/sl.json | 16 ++++++++++++ .../huawei_lte/.translations/it.json | 5 +++- .../huawei_lte/.translations/sl.json | 5 +++- .../components/lock/.translations/fr.json | 4 +++ .../components/lock/.translations/sl.json | 4 +++ .../components/vacuum/.translations/fr.json | 16 ++++++++++++ .../components/vacuum/.translations/sl.json | 16 ++++++++++++ .../components/wled/.translations/fr.json | 21 +++++++++++++++ .../components/wled/.translations/it.json | 1 + .../components/wled/.translations/sl.json | 26 +++++++++++++++++++ 18 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/climate/.translations/fr.json create mode 100644 homeassistant/components/climate/.translations/sl.json create mode 100644 homeassistant/components/fan/.translations/fr.json create mode 100644 homeassistant/components/fan/.translations/sl.json create mode 100644 homeassistant/components/vacuum/.translations/fr.json create mode 100644 homeassistant/components/vacuum/.translations/sl.json create mode 100644 homeassistant/components/wled/.translations/sl.json diff --git a/homeassistant/components/almond/.translations/fr.json b/homeassistant/components/almond/.translations/fr.json index b304a596b3a..0208366cea1 100644 --- a/homeassistant/components/almond/.translations/fr.json +++ b/homeassistant/components/almond/.translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Almond", - "cannot_connect": "Impossible de se connecter au serveur Almond" + "cannot_connect": "Impossible de se connecter au serveur Almond", + "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond." }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json index a7e207e899b..740535f4f46 100644 --- a/homeassistant/components/almond/.translations/it.json +++ b/homeassistant/components/almond/.translations/it.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "\u00c8 possibile configurare un solo account Almond.", - "cannot_connect": "Impossibile connettersi al server Almond." + "cannot_connect": "Impossibile connettersi al server Almond.", + "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond." }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json index f8bf3021db5..c809b908b9f 100644 --- a/homeassistant/components/almond/.translations/sl.json +++ b/homeassistant/components/almond/.translations/sl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Konfigurirate lahko samo en ra\u010dun Almond.", - "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond." + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond.", + "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." }, "title": "Almond" } diff --git a/homeassistant/components/climate/.translations/fr.json b/homeassistant/components/climate/.translations/fr.json new file mode 100644 index 00000000000..d82a3644493 --- /dev/null +++ b/homeassistant/components/climate/.translations/fr.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "set_preset_mode": "Changer les pr\u00e9r\u00e9glages de {entity_name}" + }, + "trigger_type": { + "current_humidity_changed": "Changement d'humidit\u00e9 mesur\u00e9e pour {entity_name}", + "current_temperature_changed": "Changement de temp\u00e9rature mesur\u00e9e pour {entity_name}", + "hvac_mode_changed": "Mode HVAC chang\u00e9 pour {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/sl.json b/homeassistant/components/climate/.translations/sl.json new file mode 100644 index 00000000000..4ba4cb02a4b --- /dev/null +++ b/homeassistant/components/climate/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Spremeni na\u010din HVAC na {entity_name}", + "set_preset_mode": "Spremenite prednastavitev na {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} je nastavljen na dolo\u010den na\u010din HVAC", + "is_preset_mode": "{entity_name} je nastavljen na dolo\u010den prednastavljeni na\u010din" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} spremenjena izmerjena vla\u017enost", + "current_temperature_changed": "{entity_name} izmerjena temperaturna sprememba", + "hvac_mode_changed": "{entity_name} HVAC na\u010din spremenjen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index e2e0d529064..33d49dfca46 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -81,7 +81,11 @@ "remote_gyro_activated": "Dispositivo in vibrazione", "remote_moved": "Dispositivo spostato con \"{subtype}\" verso l'alto", "remote_rotate_from_side_1": "Dispositivo ruotato da \"lato 1\" a \"{subtype}\"", - "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"" + "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositivo ruotato da \"lato 3\" a \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositivo ruotato da \"lato 4\" a \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositivo ruotato da \"lato 5\" a \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositivo ruotato da \"lato 6\" a \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 217007c07d4..0edfc11af55 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -55,10 +55,17 @@ "left": "Levo", "open": "Odprto", "right": "Desno", + "side_1": "Stran 1", + "side_2": "Stran 2", + "side_3": "Stran 3", + "side_4": "Stran 4", + "side_5": "Stran 5", + "side_6": "Stran 6", "turn_off": "Ugasni", "turn_on": "Pri\u017egi" }, "trigger_type": { + "remote_awakened": "Naprava se je prebudila", "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", @@ -69,7 +76,16 @@ "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", - "remote_gyro_activated": "Naprava se je pretresla" + "remote_double_tap": "Naprava \"{subtype}\" dvakrat dotaknjena", + "remote_falling": "Naprava v prostem padu", + "remote_gyro_activated": "Naprava se je pretresla", + "remote_moved": "Naprava je premaknjena s \"{subtype}\" navzgor", + "remote_rotate_from_side_1": "Naprava je zasukana iz \"strani 1\" v \"{subtype}\"", + "remote_rotate_from_side_2": "Naprava je zasukana iz \"strani 2\" v \"{subtype}\"", + "remote_rotate_from_side_3": "Naprava je zasukana iz \"strani 3\" v \"{subtype}\"", + "remote_rotate_from_side_4": "Naprava je zasukana iz \"strani 4\" v \"{subtype}\"", + "remote_rotate_from_side_5": "Naprava je zasukana iz \"strani 5\" v \"{subtype}\"", + "remote_rotate_from_side_6": "Naprava je zasukana iz \"strani 6\" v \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/fan/.translations/fr.json b/homeassistant/components/fan/.translations/fr.json new file mode 100644 index 00000000000..5c5a65b6bcd --- /dev/null +++ b/homeassistant/components/fan/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9" + }, + "trigger_type": { + "turned_off": "{entity_name} est \u00e9teint", + "turned_on": "{entity_name} allum\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/sl.json b/homeassistant/components/fan/.translations/sl.json new file mode 100644 index 00000000000..a5de109f764 --- /dev/null +++ b/homeassistant/components/fan/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Izklopite {entity_name}", + "turn_on": "Vklopite {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen" + }, + "trigger_type": { + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index 0646cd4da52..bcbae3b1b25 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato configurato" + "already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato configurato", + "already_in_progress": "Questo dispositivo \u00e8 gi\u00e0 in fase di configurazione", + "not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE" }, "error": { "connection_failed": "Connessione fallita", + "connection_timeout": "Timeout di connessione", "incorrect_password": "Password errata", "incorrect_username": "Nome utente errato", "incorrect_username_or_password": "Nome utente o password errati", diff --git a/homeassistant/components/huawei_lte/.translations/sl.json b/homeassistant/components/huawei_lte/.translations/sl.json index e23ac72bcca..0b4964069b2 100644 --- a/homeassistant/components/huawei_lte/.translations/sl.json +++ b/homeassistant/components/huawei_lte/.translations/sl.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Ta naprava je \u017ee nastavljena" + "already_configured": "Ta naprava je \u017ee nastavljena", + "already_in_progress": "Ta naprava se \u017ee nastavlja", + "not_huawei_lte": "Ni naprava Huawei LTE" }, "error": { "connection_failed": "Povezava ni uspela", + "connection_timeout": "\u010casovna omejitev povezave", "incorrect_password": "Nepravilno geslo", "incorrect_username": "Nepravilno uporabni\u0161ko ime", "incorrect_username_or_password": "Nepravilno uporabni\u0161ko ime ali geslo", diff --git a/homeassistant/components/lock/.translations/fr.json b/homeassistant/components/lock/.translations/fr.json index 748a1e9290c..cc7e7d6f3e3 100644 --- a/homeassistant/components/lock/.translations/fr.json +++ b/homeassistant/components/lock/.translations/fr.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} est verrouill\u00e9", "is_unlocked": "{entity_name} est d\u00e9verrouill\u00e9" + }, + "trigger_type": { + "locked": "{entity_name} verrouill\u00e9", + "unlocked": "{entity_name} d\u00e9verrouill\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/sl.json b/homeassistant/components/lock/.translations/sl.json index d2e32499d2e..01cf3feb4a6 100644 --- a/homeassistant/components/lock/.translations/sl.json +++ b/homeassistant/components/lock/.translations/sl.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} je/so zaklenjen/a", "is_unlocked": "{entity_name} je/so odklenjen/a" + }, + "trigger_type": { + "locked": "{entity_name} zaklenjen/a", + "unlocked": "{entity_name} odklenjen/a" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/fr.json b/homeassistant/components/vacuum/.translations/fr.json new file mode 100644 index 00000000000..44e7b2887e2 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Laisser {entity_name} vide", + "dock": "Laisser {entity_name} retourner \u00e0 la base" + }, + "condtion_type": { + "is_cleaning": "{entity_name} nettoie", + "is_docked": "{entity_name} est sur la base" + }, + "trigger_type": { + "cleaning": "{entity_name} commence \u00e0 nettoyer", + "docked": "{entity_name} est sur la base" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/sl.json b/homeassistant/components/vacuum/.translations/sl.json new file mode 100644 index 00000000000..25de303b157 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Naj {entity_name} \u010disti", + "dock": "Pustite, da se {entity_name} vrne na dok" + }, + "condtion_type": { + "is_cleaning": "{entity_name} \u010disti", + "is_docked": "{entity_name} je priklju\u010den" + }, + "trigger_type": { + "cleaning": "{entity_name} za\u010del \u010di\u0161\u010denje", + "docked": "{entity_name} priklju\u010den" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/fr.json b/homeassistant/components/wled/.translations/fr.json index a9ef8aa567a..5da30ab6288 100644 --- a/homeassistant/components/wled/.translations/fr.json +++ b/homeassistant/components/wled/.translations/fr.json @@ -1,5 +1,26 @@ { "config": { + "abort": { + "already_configured": "Cet appareil WLED est d\u00e9j\u00e0 configur\u00e9.", + "connection_error": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique WLED." + }, + "error": { + "connection_error": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique WLED." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "H\u00f4te ou adresse IP" + }, + "description": "Configurer votre WLED pour l'int\u00e9grer \u00e0 Home Assistant.", + "title": "Liez votre WLED" + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 '{name}' \u00e0 Home Assistant?", + "title": "Dispositif WLED d\u00e9couvert" + } + }, "title": "WLED" } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/it.json b/homeassistant/components/wled/.translations/it.json index 03c24101c2a..300f88ddc46 100644 --- a/homeassistant/components/wled/.translations/it.json +++ b/homeassistant/components/wled/.translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Questo dispositivo WLED \u00e8 gi\u00e0 configurato.", "connection_error": "Impossibile connettersi al dispositivo WLED." }, "error": { diff --git a/homeassistant/components/wled/.translations/sl.json b/homeassistant/components/wled/.translations/sl.json new file mode 100644 index 00000000000..b9ffb347a80 --- /dev/null +++ b/homeassistant/components/wled/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ta naprava WLED je \u017ee konfigurirana.", + "connection_error": "Povezava z napravo WLED ni uspela." + }, + "error": { + "connection_error": "Povezava z napravo WLED ni uspela." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Gostitelj ali IP naslov" + }, + "description": "Nastavite svoj WLED za integracijo s Home Assistant.", + "title": "Pove\u017eite svoj WLED" + }, + "zeroconf_confirm": { + "description": "Ali \u017eelite dodati WLED z imenom `{name}` v Home Assistant?", + "title": "Odkrite WLED naprave" + } + }, + "title": "WLED" + } +} \ No newline at end of file From cfa689c3a6b139b372283c35e59236ba1727282e Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 11 Nov 2019 20:53:23 -0500 Subject: [PATCH 262/306] Bump up ZHA dependencies. (#28711) --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index a8ef269e394..18e8af7008d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.10.0", + "bellows-homeassistant==0.11.0", "zha-quirks==0.0.27", - "zigpy-deconz==0.6.0", - "zigpy-homeassistant==0.10.0", - "zigpy-xbee-homeassistant==0.6.0", + "zigpy-deconz==0.7.0", + "zigpy-homeassistant==0.11.0", + "zigpy-xbee-homeassistant==0.7.0", "zigpy-zigate==0.5.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 01538784fec..7859072e2c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -285,7 +285,7 @@ beautifulsoup4==4.8.1 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.10.0 +bellows-homeassistant==0.11.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.2 @@ -2063,13 +2063,13 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.6.0 +zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.10.0 +zigpy-homeassistant==0.11.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.6.0 +zigpy-xbee-homeassistant==0.7.0 # homeassistant.components.zha zigpy-zigate==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d352e21b029..589d9d0bc41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.10.0 +bellows-homeassistant==0.11.0 # homeassistant.components.bom bomradarloop==0.1.3 @@ -640,13 +640,13 @@ zeroconf==0.23.0 zha-quirks==0.0.27 # homeassistant.components.zha -zigpy-deconz==0.6.0 +zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.10.0 +zigpy-homeassistant==0.11.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.6.0 +zigpy-xbee-homeassistant==0.7.0 # homeassistant.components.zha zigpy-zigate==0.5.0 From a89b4011eeeb347a95513329171d31df24cbcfd4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 11 Nov 2019 18:55:31 -0700 Subject: [PATCH 263/306] Ensure SimpliSafe alarm control panels can return from being offline (#28710) --- homeassistant/components/simplisafe/alarm_control_panel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index bbd9a5d4e51..a63a077ed15 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -133,6 +133,8 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): self._online = False return + self._online = True + if self._system.state == SystemStates.off: self._state = STATE_ALARM_DISARMED elif self._system.state in (SystemStates.home, SystemStates.home_count): From 87606bc12b34876796105ddd2e26fa60ed2cdb48 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 11 Nov 2019 21:30:07 -0600 Subject: [PATCH 264/306] Bump plexapi to 3.3.0 (#28709) * Bump plexapi to 3.2.0 * Bump to 3.3.0 --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index eaf3028b39f..29bdbf34b60 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==3.0.6", + "plexapi==3.3.0", "plexauth==0.0.5", "plexwebsocket==0.0.5" ], diff --git a/requirements_all.txt b/requirements_all.txt index 7859072e2c6..aebf0c92b22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ pillow==6.2.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==3.0.6 +plexapi==3.3.0 # homeassistant.components.plex plexauth==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 589d9d0bc41..2126fed9f6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ pilight==0.1.1 pillow==6.2.1 # homeassistant.components.plex -plexapi==3.0.6 +plexapi==3.3.0 # homeassistant.components.plex plexauth==0.0.5 From 4f11eec1a1bef997ab1ec1413bdbdd7577cf4c19 Mon Sep 17 00:00:00 2001 From: Federico Leoni Date: Tue, 12 Nov 2019 05:53:08 -0300 Subject: [PATCH 265/306] Update binary_sensor.py (#28707) * Update binary_sensor.py * Fix style issues --- homeassistant/components/mqtt/binary_sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index bcf398464bc..fe47729561d 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -190,9 +190,11 @@ class MqttBinarySensor( self._state = False else: # Payload is not for this entity _LOGGER.warning( - "No matching payload found" " for entity: %s with state_topic: %s", + "No matching payload found for entity: %s with state topic: %s. Payload: %s, with value template %s", self._config[CONF_NAME], self._config[CONF_STATE_TOPIC], + payload, + value_template, ) return From e8348221d488da6086c9db756a1c5f78db21061f Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 12 Nov 2019 11:32:32 +0100 Subject: [PATCH 266/306] Allow preset boost for Homematic IP Cloud power switches (#28713) * Allow preset boost for Homematic IP Cloud power switches * Sort Imports * Add test * align test data --- .../components/homematicip_cloud/climate.py | 18 ++++++++++++++--- .../homematicip_cloud/test_climate.py | 20 +++++++++++++++++++ tests/fixtures/homematicip_cloud.json | 16 ++++++++++----- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index a8ea424b207..9673459e820 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -5,6 +5,7 @@ from typing import Awaitable from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup from homematicip.base.enums import AbsenceType +from homematicip.device import Switch from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice @@ -116,7 +117,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): @property def hvac_mode(self) -> str: """Return hvac operation ie.""" - if self._disabled_by_cooling_mode: + if self._disabled_by_cooling_mode and not self._has_switch: return HVAC_MODE_OFF if self._device.boostMode: return HVAC_MODE_HEAT @@ -128,7 +129,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): @property def hvac_modes(self): """Return the list of available hvac operation modes.""" - if self._disabled_by_cooling_mode: + if self._disabled_by_cooling_mode and not self._has_switch: return [HVAC_MODE_OFF] return ( @@ -168,7 +169,9 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): profile_names = self._device_profile_names presets = [] - if self._heat_mode_enabled and self._has_radiator_thermostat: + if ( + self._heat_mode_enabled and self._has_radiator_thermostat + ) or self._has_switch: if not profile_names: presets.append(PRESET_NONE) presets.append(PRESET_BOOST) @@ -290,6 +293,15 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): return HEATING_PROFILES if self._heat_mode_enabled else COOLING_PROFILES + @property + def _has_switch(self) -> bool: + """Return, if a switch is in the hmip heating group.""" + for device in self._device.devices: + if isinstance(device, Switch): + return True + + return False + @property def _has_radiator_thermostat(self) -> bool: """Return, if a radiator thermostat is in the hmip heating group.""" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 6a05a880864..858fba29563 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -333,6 +333,26 @@ async def test_hmip_heating_group_cool(hass, default_mock_hap): assert hmip_device.mock_calls[-1][1] == (4,) +async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap): + """Test HomematicipHeatingGroup.""" + entity_id = "climate.schlafzimmer" + entity_name = "Schlafzimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes["current_temperature"] == 24.7 + assert ha_state.attributes["min_temp"] == 5.0 + assert ha_state.attributes["max_temp"] == 30.0 + assert ha_state.attributes["temperature"] == 5.0 + assert ha_state.attributes["current_humidity"] == 43 + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" + assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "P2"] + + async def test_hmip_climate_services(hass, mock_hap_with_service): """Test HomematicipHeatingGroup.""" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index d1ef82f4234..8cec2462f32 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -5,6 +5,12 @@ "id": "00000000-0000-0000-0000-000000000000", "label": "TEST-Client", "clientType": "APP" + }, + "AA000000-0000-0000-0000-000000000000": { + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "AA000000-0000-0000-0000-000000000000", + "label": "REMOVE_ME", + "clientType": "APP" } }, "devices": { @@ -1485,7 +1491,7 @@ "modelType": "HmIP-SMI", "oem": "eQ-3", "permanentlyReachable": true, - "serializedGlobalTradeItemNumber": "3014F7110000000000000011", + "serializedGlobalTradeItemNumber": "3014F711000000000000BB11", "type": "MOTION_DETECTOR_INDOOR", "updateState": "UP_TO_DATE" }, @@ -2202,7 +2208,7 @@ } }, "homeId": "00000000-0000-0000-0000-000000000001", - "id": "3014F7110000000000000011", + "id": "3014F7110000000000000109", "label": "Ausschalter Terrasse Bewegungsmelder", "lastStatusUpdate": 1570366291250, "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", @@ -4075,7 +4081,7 @@ }, { "channelIndex": 1, - "deviceId": "3014F7110000000000000011" + "deviceId": "3014F7110000000000000008" } ], "controlMode": "AUTOMATIC", @@ -4109,7 +4115,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000012", "index": "PROFILE_1", - "name": "", + "name": "STD", "profileId": "00000000-0000-0000-0000-000000000023", "visible": true }, @@ -4117,7 +4123,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000012", "index": "PROFILE_2", - "name": "", + "name": "P2", "profileId": "00000000-0000-0000-0000-000000000024", "visible": true }, From 1208ab4c76ad3b1390f3e0e4681826d67910256e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 12 Nov 2019 12:28:34 +0100 Subject: [PATCH 267/306] Upgrade discogs_client to 2.2.2 (#28723) --- homeassistant/components/discogs/manifest.json | 2 +- homeassistant/components/discogs/sensor.py | 12 +++--------- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index 18282f07781..5a1c670508a 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -3,7 +3,7 @@ "name": "Discogs", "documentation": "https://www.home-assistant.io/integrations/discogs", "requirements": [ - "discogs_client==2.2.1" + "discogs_client==2.2.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index b3f29fbe75b..b5e488cc19c 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -66,7 +66,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Discogs sensor.""" - token = config[CONF_TOKEN] name = config[CONF_NAME] @@ -104,7 +103,7 @@ class DiscogsSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, SENSORS[self._type]["name"]) + return f"{self._name} {SENSORS[self._type]['name']}" @property def state(self): @@ -136,10 +135,7 @@ class DiscogsSensor(Entity): 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], - ), + "format": f"{self._attrs['formats'][0]['name']} ({self._attrs['formats'][0]['descriptions'][0]})", "label": self._attrs["labels"][0]["name"], "released": self._attrs["year"], ATTR_ATTRIBUTION: ATTRIBUTION, @@ -154,9 +150,7 @@ class DiscogsSensor(Entity): random_record = collection.releases[random_index].release self._attrs = random_record.data - return "{} - {}".format( - random_record.data["artists"][0]["name"], random_record.data["title"] - ) + return f"{random_record.data['artists'][0]['name']} - {random_record.data['title']}" def update(self): """Set state to the amount of records in user's collection.""" diff --git a/requirements_all.txt b/requirements_all.txt index aebf0c92b22..c907fd879a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -414,7 +414,7 @@ denonavr==0.7.10 directpy==0.5 # homeassistant.components.discogs -discogs_client==2.2.1 +discogs_client==2.2.2 # homeassistant.components.discord discord.py==1.2.4 From 48fd95c7dbfa0b870dc5f18e315801c6174981cf Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Tue, 12 Nov 2019 15:04:17 +0100 Subject: [PATCH 268/306] Fix Here Travel Time unable to find entity on startup (#27237) * add support for entity_id as state of entity * add circular reference detection * voluptuous instead of regex * wait for EVENT_HOMEASSISTANT_START * move delayed_sensor_update to async_added_to_hass * add @callback decorator * remove nested entity resolving --- .../components/here_travel_time/sensor.py | 75 ++++++--- .../here_travel_time/test_sensor.py | 149 +++++++++++++++++- 2 files changed, 198 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index afd12bb01c5..0b688a770c5 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -17,8 +17,9 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_START, ) -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -89,8 +90,6 @@ UNIT_OF_MEASUREMENT = "min" SCAN_INTERVAL = timedelta(minutes=5) -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] - NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" PLATFORM_SCHEMA = vol.All( @@ -146,15 +145,19 @@ async def async_setup_platform( if config.get(CONF_ORIGIN_LATITUDE) is not None: origin = f"{config[CONF_ORIGIN_LATITUDE]},{config[CONF_ORIGIN_LONGITUDE]}" + origin_entity_id = None else: - origin = config[CONF_ORIGIN_ENTITY_ID] + origin = None + origin_entity_id = config[CONF_ORIGIN_ENTITY_ID] if config.get(CONF_DESTINATION_LATITUDE) is not None: destination = ( f"{config[CONF_DESTINATION_LATITUDE]},{config[CONF_DESTINATION_LONGITUDE]}" ) + destination_entity_id = None else: - destination = config[CONF_DESTINATION_ENTITY_ID] + destination = None + destination_entity_id = config[CONF_DESTINATION_ENTITY_ID] travel_mode = config[CONF_MODE] traffic_mode = config[CONF_TRAFFIC_MODE] @@ -166,9 +169,11 @@ async def async_setup_platform( here_client, travel_mode, traffic_mode, route_mode, units ) - sensor = HERETravelTimeSensor(name, origin, destination, here_data) + sensor = HERETravelTimeSensor( + name, origin, destination, origin_entity_id, destination_entity_id, here_data + ) - async_add_entities([sensor], True) + async_add_entities([sensor]) def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: @@ -194,31 +199,43 @@ class HERETravelTimeSensor(Entity): """Representation of a HERE travel time sensor.""" def __init__( - self, name: str, origin: str, destination: str, here_data: "HERETravelTimeData" + self, + name: str, + origin: str, + destination: str, + origin_entity_id: str, + destination_entity_id: str, + here_data: "HERETravelTimeData", ) -> None: """Initialize the sensor.""" self._name = name + self._origin_entity_id = origin_entity_id + self._destination_entity_id = destination_entity_id self._here_data = here_data self._unit_of_measurement = UNIT_OF_MEASUREMENT - self._origin_entity_id = None - self._destination_entity_id = None self._attrs = { ATTR_UNIT_SYSTEM: self._here_data.units, ATTR_MODE: self._here_data.travel_mode, ATTR_TRAFFIC_MODE: self._here_data.traffic_mode, } - - # Check if location is a trackable entity - if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: - self._origin_entity_id = origin - else: + if self._origin_entity_id is None: self._here_data.origin = origin - if destination.split(".", 1)[0] in TRACKABLE_DOMAINS: - self._destination_entity_id = destination - else: + if self._destination_entity_id is None: self._here_data.destination = destination + async def async_added_to_hass(self) -> None: + """Delay the sensor update to avoid entity not found warnings.""" + + @callback + def delayed_sensor_update(event): + """Update sensor after homeassistant started.""" + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, delayed_sensor_update + ) + @property def state(self) -> Optional[str]: """Return the state of the sensor.""" @@ -309,10 +326,28 @@ class HERETravelTimeSensor(Entity): ) return self._get_location_from_attributes(zone_entity) - # If zone was not found in state then use the state as the location - if entity_id.startswith("sensor."): + # Check if state is valid coordinate set + if self._entity_state_is_valid_coordinate_set(entity.state): return entity.state + _LOGGER.error( + "The state of %s is not a valid set of coordinates: %s", + entity_id, + entity.state, + ) + return None + + @staticmethod + def _entity_state_is_valid_coordinate_set(state: str) -> bool: + """Check that the given string is a valid set of coordinates.""" + schema = vol.Schema(cv.gps) + try: + coordinates = state.split(",") + schema(coordinates) + return True + except (vol.MultipleInvalid): + return False + @staticmethod def _get_location_from_attributes(entity: State) -> str: """Get the lat/long string from an entities attributes.""" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 783209690a3..1d788c93c66 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -38,7 +38,7 @@ from homeassistant.components.here_travel_time.sensor import ( TRAVEL_MODE_TRUCK, UNIT_OF_MEASUREMENT, ) -from homeassistant.const import ATTR_ICON +from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -176,13 +176,14 @@ async def test_car(hass, requests_mock_car_disabled_response): "app_code": APP_CODE, } } - assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "30" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT - assert sensor.attributes.get(ATTR_ATTRIBUTION) is None assert sensor.attributes.get(ATTR_DURATION) == 30.05 assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 @@ -241,8 +242,10 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) - sensor = hass.states.get("sensor.test") + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") # Test traffic mode enabled assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( ATTR_DURATION_IN_TRAFFIC @@ -266,6 +269,9 @@ async def test_imperial(hass, requests_mock_car_disabled_response): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994 @@ -295,6 +301,9 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.attributes.get(ATTR_DISTANCE) == 18.388 @@ -324,6 +333,9 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.attributes.get(ATTR_DISTANCE) == 23.381 @@ -344,6 +356,10 @@ async def test_truck(hass, requests_mock_truck_response): } } assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -373,6 +389,9 @@ async def test_public_transport(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "89" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT @@ -421,6 +440,9 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "80" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT @@ -466,8 +488,12 @@ async def test_pedestrian(hass, requests_mock_credentials_check): "mode": TRAVEL_MODE_PEDESTRIAN, } } + assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "211" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT @@ -517,6 +543,9 @@ async def test_bicycle(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "55" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT @@ -564,8 +593,6 @@ async def test_location_zone(hass, requests_mock_truck_response): }, ] } - assert await async_setup_component(hass, "zone", zone_config) - config = { DOMAIN: { "platform": PLATFORM, @@ -577,8 +604,12 @@ async def test_location_zone(hass, requests_mock_truck_response): "mode": TRAVEL_MODE_TRUCK, } } + assert await async_setup_component(hass, "zone", zone_config) assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -616,6 +647,9 @@ async def test_location_sensor(hass, requests_mock_truck_response): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -662,6 +696,9 @@ async def test_location_person(hass, requests_mock_truck_response): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -708,6 +745,9 @@ async def test_location_device_tracker(hass, requests_mock_truck_response): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -740,6 +780,9 @@ async def test_location_device_tracker_added_after_update( } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert len(caplog.records) == 2 assert "Unable to find entity" in caplog.text @@ -806,6 +849,9 @@ async def test_location_device_tracker_in_zone( } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) assert ", getting zone location" in caplog.text @@ -836,6 +882,10 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog): } } assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(caplog.records) == 1 assert NO_ROUTE_ERROR_MESSAGE in caplog.text @@ -940,8 +990,95 @@ async def test_attribution(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert ( sensor.attributes.get(ATTR_ATTRIBUTION) == "With the support of HERE Technologies. All information is provided without warranty of any kind." ) + + +async def test_pattern_entity_state(hass, requests_mock_truck_response, caplog): + """Test that pattern matching the state of an entity works.""" + caplog.set_level(logging.ERROR) + hass.states.async_set("sensor.origin", "invalid") + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "sensor.origin", + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(caplog.records) == 1 + assert "is not a valid set of coordinates" in caplog.text + + +async def test_pattern_entity_state_with_space(hass, requests_mock_truck_response): + """Test that pattern matching the state including a space of an entity works.""" + hass.states.async_set( + "sensor.origin", ", ".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "sensor.origin", + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + +async def test_delayed_update(hass, requests_mock_truck_response, caplog): + """Test that delayed update does not complain about missing entities.""" + caplog.set_level(logging.WARNING) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "sensor.origin", + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + sensor_config = { + "sensor": { + "platform": "template", + "sensors": [ + {"template_sensor": {"value_template": "{{states('sensor.origin')}}"}} + ], + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, "sensor", sensor_config) + hass.states.async_set( + "sensor.origin", ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert "Unable to find entity" not in caplog.text From 5f177fa42e48f298400b45725926da6f2ffb8e72 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 12 Nov 2019 10:05:30 -0600 Subject: [PATCH 269/306] Use library method for season number (#28708) --- homeassistant/components/plex/media_player.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1fe83928d29..d6720fd9e95 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -289,12 +289,7 @@ class PlexMediaPlayer(MediaPlayerDevice): self._media_content_type = MEDIA_TYPE_TVSHOW # season number (00) - if callable(self.session.season): - self._media_season = str((self.session.season()).index).zfill(2) - elif self.session.parentIndex is not None: - self._media_season = self.session.parentIndex.zfill(2) - else: - self._media_season = None + self._media_season = self.session.seasonNumber # show name self._media_series_title = self.session.grandparentTitle # episode number (00) From fc04b3e31c465348f5242cfebf2776c0d2309301 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Nov 2019 09:22:28 -0800 Subject: [PATCH 270/306] Remove choice word when Almond has choices (#28725) --- homeassistant/components/almond/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 9977d48ae9a..a1983288f92 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -243,6 +243,7 @@ class AlmondAgent(conversation.AbstractConversationAgent): """Process a sentence.""" response = await self.api.async_converse_text(text, conversation_id) + first_choice = True buffer = "" for message in response["messages"]: if message["type"] == "text": @@ -257,7 +258,11 @@ class AlmondAgent(conversation.AbstractConversationAgent): + message["rdl"]["webCallback"] ) elif message["type"] == "choice": - buffer += "\n Choice: " + message["title"] + if first_choice: + first_choice = False + else: + buffer += "," + buffer += f" {message['title']}" intent_result = intent.IntentResponse() intent_result.async_set_speech(buffer.strip()) From a1f2b6d402ff249a3a27017c20a6439722504f23 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 18:26:46 +0100 Subject: [PATCH 271/306] ESPHome fix missing state in certain circumstances (#28729) * Fix ESPHome having missing state in certain situations Fixes https://github.com/esphome/issues/issues/828 * Update requirements_all * Also fix climate preset mode --- homeassistant/components/esphome/binary_sensor.py | 2 ++ homeassistant/components/esphome/climate.py | 2 +- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 4e684638bb7..64506f69283 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -44,6 +44,8 @@ class EsphomeBinarySensor(EsphomeEntity, BinarySensorDevice): return self._entry_data.available if self._state is None: return None + if self._state.missing_state: + return None return self._state.state @property diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 1dfe2184952..5fed8da76ef 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -138,7 +138,7 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): @esphome_state_property def preset_mode(self): """Return current preset mode.""" - return PRESET_AWAY if self._state.away else None + return PRESET_AWAY if self._state.away else PRESET_HOME @esphome_state_property def current_temperature(self) -> Optional[float]: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 40691c653f5..724946e6984 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.4.2" + "aioesphomeapi==2.5.0" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index b6adbf93c41..e50991af6c1 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -67,6 +67,8 @@ class EsphomeSensor(EsphomeEntity): """Return the state of the entity.""" if math.isnan(self._state.state): return None + if self._state.missing_state: + return None return "{:.{prec}f}".format( self._state.state, prec=self._static_info.accuracy_decimals ) @@ -96,4 +98,6 @@ class EsphomeTextSensor(EsphomeEntity): @esphome_state_property def state(self) -> Optional[str]: """Return the state of the entity.""" + if self._state.missing_state: + return None return self._state.state diff --git a/requirements_all.txt b/requirements_all.txt index c907fd879a2..163918f5180 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -142,7 +142,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.4.2 +aioesphomeapi==2.5.0 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2126fed9f6f..90668e3eaab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -50,7 +50,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.4.2 +aioesphomeapi==2.5.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 5961215e6e52131e0d59cdaf6a8aae9e2a0c0b94 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Nov 2019 11:01:19 -0800 Subject: [PATCH 272/306] Set up Almond Web to connect to HA (#28603) * Set up Almond Web to connect to HA * Add missing string * Add type --- homeassistant/components/almond/__init__.py | 69 ++++++++--- homeassistant/components/almond/strings.json | 5 + homeassistant/helpers/network.py | 38 +++++++ tests/components/almond/test_init.py | 113 +++++++++++++++++++ tests/helpers/test_network.py | 34 ++++++ 5 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 homeassistant/helpers/network.py create mode 100644 tests/components/almond/test_init.py create mode 100644 tests/helpers/test_network.py diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index a1983288f92..6d4ab31bf17 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -10,16 +10,18 @@ from aiohttp import ClientSession, ClientError from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI import voluptuous as vol -from homeassistant import core -from homeassistant.const import CONF_TYPE, CONF_HOST +from homeassistant.core import HomeAssistant, CoreState +from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.helpers import ( config_validation as cv, config_entry_oauth2_flow, + event, intent, aiohttp_client, storage, + network, ) from homeassistant import config_entries from homeassistant.components import conversation @@ -33,6 +35,8 @@ CONF_CLIENT_SECRET = "client_secret" STORAGE_VERSION = 1 STORAGE_KEY = DOMAIN +ALMOND_SETUP_DELAY = 30 + DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu" DEFAULT_LOCAL_HOST = "http://localhost:3000" @@ -93,7 +97,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Set up Almond config entry.""" websession = aiohttp_client.async_get_clientsession(hass) @@ -112,12 +116,50 @@ async def async_setup_entry(hass, entry): api = WebAlmondAPI(auth) agent = AlmondAgent(hass, api, entry) - # Hass.io does its own configuration of Almond. - if entry.data.get("is_hassio") or entry.data["type"] != TYPE_LOCAL: - conversation.async_set_agent(hass, agent) - return True + # Hass.io does its own configuration. + if not entry.data.get("is_hassio"): + # If we're not starting or local, set up Almond right away + if hass.state != CoreState.not_running or entry.data["type"] == TYPE_LOCAL: + await _configure_almond_for_ha(hass, entry, api) - # Configure Almond to connect to Home Assistant + else: + # OAuth2 implementations can potentially rely on the HA Cloud url. + # This url is not be available until 30 seconds after boot. + + async def configure_almond(_now): + try: + await _configure_almond_for_ha(hass, entry, api) + except ConfigEntryNotReady: + _LOGGER.warning( + "Unable to configure Almond to connect to Home Assistant" + ) + + async def almond_hass_start(_event): + event.async_call_later(hass, ALMOND_SETUP_DELAY, configure_almond) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, almond_hass_start) + + conversation.async_set_agent(hass, agent) + return True + + +async def _configure_almond_for_ha( + hass: HomeAssistant, entry: config_entries.ConfigEntry, api: WebAlmondAPI +): + """Configure Almond to connect to HA.""" + + if entry.data["type"] == TYPE_OAUTH2: + # If we're connecting over OAuth2, we will only set up connection + # with Home Assistant if we're remotely accessible. + hass_url = network.async_get_external_url(hass) + else: + hass_url = hass.config.api.base_url + + # If hass_url is None, we're not going to configure Almond to connect to HA. + if hass_url is None: + return + + _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) data = await store.async_load() @@ -144,11 +186,11 @@ async def async_setup_entry(hass, entry): # Store token in Almond try: - with async_timeout.timeout(10): + with async_timeout.timeout(30): await api.async_create_device( { "kind": "io.home-assistant", - "hassUrl": hass.config.api.base_url, + "hassUrl": hass_url, "accessToken": access_token, "refreshToken": "", # 5 years from now in ms. @@ -169,9 +211,6 @@ async def async_setup_entry(hass, entry): if token.id != refresh_token.id: await hass.auth.async_remove_refresh_token(token) - conversation.async_set_agent(hass, agent) - return True - async def async_unload_entry(hass, entry): """Unload Almond.""" @@ -203,7 +242,9 @@ class AlmondOAuth(AbstractAlmondWebAuth): class AlmondAgent(conversation.AbstractConversationAgent): """Almond conversation agent.""" - def __init__(self, hass: core.HomeAssistant, api: WebAlmondAPI, entry): + def __init__( + self, hass: HomeAssistant, api: WebAlmondAPI, entry: config_entries.ConfigEntry + ): """Initialize the agent.""" self.hass = hass self.api = api diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index 5cfc52044bb..872367eb862 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -1,5 +1,10 @@ { "config": { + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, "abort": { "already_setup": "You can only configure one Almond account.", "cannot_connect": "Unable to connect to the Almond server.", diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py new file mode 100644 index 00000000000..671e7f1fa56 --- /dev/null +++ b/homeassistant/helpers/network.py @@ -0,0 +1,38 @@ +"""Network helpers.""" +from typing import Optional, cast +from ipaddress import ip_address + +import yarl + +from homeassistant.core import HomeAssistant, callback +from homeassistant.loader import bind_hass +from homeassistant.util.network import is_local + + +@bind_hass +@callback +def async_get_external_url(hass: HomeAssistant) -> Optional[str]: + """Get external url of this instance. + + Note: currently it takes 30 seconds after Home Assistant starts for + cloud.async_remote_ui_url to work. + """ + if "cloud" in hass.config.components: + try: + return cast(str, hass.components.cloud.async_remote_ui_url()) + except hass.components.cloud.CloudNotAvailable: + pass + + if hass.config.api is None: + return None + + base_url = yarl.URL(hass.config.api.base_url) + + try: + if is_local(ip_address(base_url.host)): + return None + except ValueError: + # ip_address raises ValueError if host is not an IP address + pass + + return str(base_url) diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py new file mode 100644 index 00000000000..dd44ea1c8f0 --- /dev/null +++ b/tests/components/almond/test_init.py @@ -0,0 +1,113 @@ +"""Tests for Almond set up.""" +from unittest.mock import patch +from time import time + +import pytest + +from homeassistant import config_entries, core +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component +from homeassistant.components.almond import const +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, mock_coro, async_fire_time_changed + + +@pytest.fixture(autouse=True) +def patch_hass_state(hass): + """Mock the hass.state to be not_running.""" + hass.state = core.CoreState.not_running + + +async def test_set_up_oauth_remote_url(hass, aioclient_mock): + """Test we set up Almond to connect to HA if we have external url.""" + entry = MockConfigEntry( + domain="almond", + data={ + "type": const.TYPE_OAUTH2, + "auth_implementation": "local", + "host": "http://localhost:9999", + "token": {"expires_at": time() + 1000, "access_token": "abcd"}, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + return_value=mock_coro(), + ): + assert await async_setup_component(hass, "almond", {}) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + + with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch( + "homeassistant.helpers.network.async_get_external_url", + return_value="https://example.nabu.casa", + ), patch( + "pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro() + ) as mock_create_device: + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + assert len(mock_create_device.mock_calls) == 1 + + +async def test_set_up_oauth_no_external_url(hass, aioclient_mock): + """Test we do not set up Almond to connect to HA if we have no external url.""" + entry = MockConfigEntry( + domain="almond", + data={ + "type": const.TYPE_OAUTH2, + "auth_implementation": "local", + "host": "http://localhost:9999", + "token": {"expires_at": time() + 1000, "access_token": "abcd"}, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + return_value=mock_coro(), + ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: + assert await async_setup_component(hass, "almond", {}) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(mock_create_device.mock_calls) == 0 + + +async def test_set_up_hassio(hass, aioclient_mock): + """Test we do not set up Almond to connect to HA if we use hassio.""" + entry = MockConfigEntry( + domain="almond", + data={ + "is_hassio": True, + "type": const.TYPE_LOCAL, + "host": "http://localhost:9999", + }, + ) + entry.add_to_hass(hass) + + with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: + assert await async_setup_component(hass, "almond", {}) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(mock_create_device.mock_calls) == 0 + + +async def test_set_up_local(hass, aioclient_mock): + """Test we do not set up Almond to connect to HA if we use hassio.""" + entry = MockConfigEntry( + domain="almond", + data={"type": const.TYPE_LOCAL, "host": "http://localhost:9999"}, + ) + entry.add_to_hass(hass) + + with patch( + "pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro() + ) as mock_create_device: + assert await async_setup_component(hass, "almond", {}) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(mock_create_device.mock_calls) == 1 diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py new file mode 100644 index 00000000000..afb9e88c5a4 --- /dev/null +++ b/tests/helpers/test_network.py @@ -0,0 +1,34 @@ +"""Test network helper.""" +from unittest.mock import Mock, patch + +from homeassistant.helpers import network +from homeassistant.components import cloud + + +async def test_get_external_url(hass): + """Test get_external_url.""" + hass.config.api = Mock(base_url="http://192.168.1.100:8123") + + assert network.async_get_external_url(hass) is None + + hass.config.api = Mock(base_url="http://example.duckdns.org:8123") + + assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" + + hass.config.components.add("cloud") + + assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" + + with patch.object( + hass.components.cloud, + "async_remote_ui_url", + side_effect=cloud.CloudNotAvailable, + ): + assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" + + with patch.object( + hass.components.cloud, + "async_remote_ui_url", + return_value="https://example.nabu.casa", + ): + assert network.async_get_external_url(hass) == "https://example.nabu.casa" From d2d9f09f13e40a8cae9c433e918b53a13f5440a1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 13 Nov 2019 00:32:12 +0000 Subject: [PATCH 273/306] [ci skip] Translation update --- .../components/almond/.translations/en.json | 5 +++ .../components/almond/.translations/es.json | 15 +++++++ .../cert_expiry/.translations/es.json | 4 +- .../components/climate/.translations/es.json | 17 ++++++++ .../coolmaster/.translations/es.json | 23 ++++++++++ .../components/cover/.translations/es.json | 12 +++++- .../components/deconz/.translations/es.json | 18 +++++++- .../device_tracker/.translations/es.json | 8 ++++ .../components/fan/.translations/es.json | 16 +++++++ .../huawei_lte/.translations/es.json | 42 +++++++++++++++++++ .../components/lock/.translations/es.json | 4 ++ .../media_player/.translations/es.json | 11 +++++ .../components/sensor/.translations/es.json | 8 ++-- .../components/solarlog/.translations/es.json | 21 ++++++++++ .../components/somfy/.translations/es.json | 5 +++ .../transmission/.translations/es.json | 5 ++- .../components/vacuum/.translations/es.json | 16 +++++++ .../components/withings/.translations/es.json | 7 ++++ .../components/wled/.translations/es.json | 26 ++++++++++++ 19 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/almond/.translations/es.json create mode 100644 homeassistant/components/climate/.translations/es.json create mode 100644 homeassistant/components/coolmaster/.translations/es.json create mode 100644 homeassistant/components/device_tracker/.translations/es.json create mode 100644 homeassistant/components/fan/.translations/es.json create mode 100644 homeassistant/components/huawei_lte/.translations/es.json create mode 100644 homeassistant/components/media_player/.translations/es.json create mode 100644 homeassistant/components/solarlog/.translations/es.json create mode 100644 homeassistant/components/vacuum/.translations/es.json create mode 100644 homeassistant/components/wled/.translations/es.json diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json index 3ee811b8326..3b7b5b9aa63 100644 --- a/homeassistant/components/almond/.translations/en.json +++ b/homeassistant/components/almond/.translations/en.json @@ -5,6 +5,11 @@ "cannot_connect": "Unable to connect to the Almond server.", "missing_configuration": "Please check the documentation on how to set up Almond." }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/es.json b/homeassistant/components/almond/.translations/es.json new file mode 100644 index 00000000000..26eacb834b0 --- /dev/null +++ b/homeassistant/components/almond/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo puede configurar una cuenta de Almond.", + "cannot_connect": "No se puede conectar al servidor Almond.", + "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json index b10518646ac..4432edac563 100644 --- a/homeassistant/components/cert_expiry/.translations/es.json +++ b/homeassistant/components/cert_expiry/.translations/es.json @@ -4,10 +4,12 @@ "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" }, "error": { + "certificate_error": "El certificado no pudo ser validado", "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", "connection_timeout": "Tiempo de espera agotado al conectar a este host", "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", - "resolve_failed": "Este host no se puede resolver" + "resolve_failed": "Este host no se puede resolver", + "wrong_host": "El certificado no coincide con el nombre de host" }, "step": { "user": { diff --git a/homeassistant/components/climate/.translations/es.json b/homeassistant/components/climate/.translations/es.json new file mode 100644 index 00000000000..baae9b97436 --- /dev/null +++ b/homeassistant/components/climate/.translations/es.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Cambiar el modo HVAC de {entity_name}.", + "set_preset_mode": "Cambiar la configuraci\u00f3n prefijada de {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", + "is_preset_mode": "{entity_name} se establece en un modo predeterminado espec\u00edfico" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} humedad medida cambi\u00f3", + "current_temperature_changed": "{entity_name} temperatura medida cambi\u00f3", + "hvac_mode_changed": "{entity_name} Modo HVAC cambiado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/es.json b/homeassistant/components/coolmaster/.translations/es.json new file mode 100644 index 00000000000..aedd81baccc --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Error al conectarse a la instancia de CoolMasterNet. Por favor revise su anfitri\u00f3n.", + "no_units": "No se ha encontrado ninguna unidad HVAC en el host CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Soporta el modo de enfriamiento", + "dry": "Soporta el modo seco", + "fan_only": "Soporta modo solo ventilador", + "heat": "Soporta modo calor", + "heat_cool": "Soporta el modo autom\u00e1tico de calor/fr\u00edo", + "host": "Host", + "off": "Se puede apagar" + }, + "title": "Configure los detalles de su conexi\u00f3n a CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/es.json b/homeassistant/components/cover/.translations/es.json index d0193b939a5..490583b54c4 100644 --- a/homeassistant/components/cover/.translations/es.json +++ b/homeassistant/components/cover/.translations/es.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} est\u00e1 cerrado", "is_closing": "{entity_name} se est\u00e1 cerrando", "is_open": "{entity_name} est\u00e1 abierto", - "is_opening": "{entity_name} se est\u00e1 abriendo" + "is_opening": "{entity_name} se est\u00e1 abriendo", + "is_position": "La posici\u00f3n actual de {entity_name} es", + "is_tilt_position": "La posici\u00f3n de inclinaci\u00f3n actual de {entity_name} es" + }, + "trigger_type": { + "closed": "{entity_name} cerrado", + "closing": "{entity_name} cerrando", + "opened": "abierto {entity_name}", + "opening": "abriendo {entity_name}", + "position": "Posici\u00f3n cambiada de {entity_name}", + "tilt_position": "Cambia la posici\u00f3n de inclinaci\u00f3n de {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index d4f8de9f282..47fd99c48a2 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -55,10 +55,17 @@ "left": "Izquierda", "open": "Abierto", "right": "Derecha", + "side_1": "Lado 1", + "side_2": "Lado 2", + "side_3": "Lado 3", + "side_4": "Lado 4", + "side_5": "Lado 5", + "side_6": "Lado 6", "turn_off": "Apagar", "turn_on": "Encender" }, "trigger_type": { + "remote_awakened": "Dispositivo despertado", "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas", "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado", @@ -69,7 +76,16 @@ "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", - "remote_gyro_activated": "Dispositivo sacudido" + "remote_double_tap": "Dispositivo \" {subtype} \" doble pulsaci\u00f3n", + "remote_falling": "Dispositivo en ca\u00edda libre", + "remote_gyro_activated": "Dispositivo sacudido", + "remote_moved": "Dispositivo movido con \"{subtipo}\" hacia arriba", + "remote_rotate_from_side_1": "Dispositivo girado del \"lado 1\" al \" {subtype} \"", + "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \" {subtype} \"", + "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \" {subtype} \"", + "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \" {subtype} \"", + "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \" {subtype} \"", + "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/es.json b/homeassistant/components/device_tracker/.translations/es.json new file mode 100644 index 00000000000..00bda928b56 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/es.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} est\u00e1 en casa", + "is_not_home": "{entity_name} no est\u00e1 en casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/es.json b/homeassistant/components/fan/.translations/es.json new file mode 100644 index 00000000000..d92153a6302 --- /dev/null +++ b/homeassistant/components/fan/.translations/es.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desactivar {entity_name}", + "turn_on": "Activar {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} est\u00e1 desactivado", + "is_on": "{entity_name} est\u00e1 activado" + }, + "trigger_type": { + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/es.json b/homeassistant/components/huawei_lte/.translations/es.json new file mode 100644 index 00000000000..92ccf8fc048 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/es.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo ya ha sido configurado", + "already_in_progress": "Este dispositivo ya se est\u00e1 configurando", + "not_huawei_lte": "No es un dispositivo Huawei LTE" + }, + "error": { + "connection_failed": "Fallo de conexi\u00f3n", + "connection_timeout": "Tiempo de espera de la conexi\u00f3n superado", + "incorrect_password": "Contrase\u00f1a incorrecta", + "incorrect_username": "Nombre de usuario incorrecto", + "incorrect_username_or_password": "Nombre de usuario o contrase\u00f1a incorrectos", + "invalid_url": "URL no v\u00e1lida", + "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, int\u00e9ntelo de nuevo m\u00e1s tarde.", + "response_error": "Error desconocido del dispositivo", + "unknown_connection_error": "Error desconocido al conectarse al dispositivo" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Nombre de usuario" + }, + "description": "Introduzca los detalles de acceso al dispositivo. La especificaci\u00f3n del nombre de usuario y la contrase\u00f1a es opcional, pero permite admitir m\u00e1s funciones de integraci\u00f3n. Por otro lado, el uso de una conexi\u00f3n autorizada puede causar problemas para acceder a la interfaz web del dispositivo desde fuera de Home Assistant mientras la integraci\u00f3n est\u00e1 activa, y viceversa.", + "title": "Configurar Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Destinatarios de notificaciones por SMS", + "track_new_devices": "Rastrea nuevos dispositivos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/es.json b/homeassistant/components/lock/.translations/es.json index c6ef789e9cb..0c352d9608a 100644 --- a/homeassistant/components/lock/.translations/es.json +++ b/homeassistant/components/lock/.translations/es.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} est\u00e1 bloqueado", "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + }, + "trigger_type": { + "locked": "{entity_name} bloqueado", + "unlocked": "{entity_name} desbloqueado" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/es.json b/homeassistant/components/media_player/.translations/es.json new file mode 100644 index 00000000000..16242dadeb6 --- /dev/null +++ b/homeassistant/components/media_player/.translations/es.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est\u00e1 inactivo", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 activado", + "is_paused": "{entity_name} est\u00e1 en pausa", + "is_playing": "{entity_name} est\u00e1 jugando" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json index a9039d2e410..c5641d38cc0 100644 --- a/homeassistant/components/sensor/.translations/es.json +++ b/homeassistant/components/sensor/.translations/es.json @@ -17,10 +17,10 @@ "illuminance": "{entity_name} iluminancia", "power": "{entity_name} alimentaci\u00f3n", "pressure": "{entity_name} presi\u00f3n", - "signal_strength": "{entity_name} intensidad de la se\u00f1al", - "temperature": "{entity_name} temperatura", - "timestamp": "{entity_name} marca de tiempo", - "value": "{entity_name} valor" + "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name} ", + "temperature": "{entity_name} cambios de temperatura", + "timestamp": "{entity_name} cambios de fecha y hora", + "value": "Cambios de valor de la {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/es.json b/homeassistant/components/solarlog/.translations/es.json new file mode 100644 index 00000000000..2e6778ffbf3 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "Si no se ha conectado, verifique la direcci\u00f3n del host" + }, + "step": { + "user": { + "data": { + "host": "El nombre del host o la direcci\u00f3n IP de su dispositivo Solar-Log", + "name": "El prefijo que se utilizar\u00e1 para los sensores Solar-Log" + }, + "title": "Defina su conexi\u00f3n Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/es.json b/homeassistant/components/somfy/.translations/es.json index fa4156d886d..d20eb71cc55 100644 --- a/homeassistant/components/somfy/.translations/es.json +++ b/homeassistant/components/somfy/.translations/es.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Autenticado correctamente con Somfy." }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/es.json b/homeassistant/components/transmission/.translations/es.json index f210eb42f8a..06ea19e72b8 100644 --- a/homeassistant/components/transmission/.translations/es.json +++ b/homeassistant/components/transmission/.translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "El host ya est\u00e1 configurado.", "one_instance_allowed": "S\u00f3lo se necesita una sola instancia." }, "error": { "cannot_connect": "No se puede conectar al host", + "name_exists": "El nombre ya existe", "wrong_credentials": "Nombre de usuario o contrase\u00f1a incorrectos" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Frecuencia de actualizaci\u00f3n" }, - "description": "Configurar opciones para la transmisi\u00f3n" + "description": "Configurar opciones para la transmisi\u00f3n", + "title": "Configurar opciones para la transmisi\u00f3n" } } } diff --git a/homeassistant/components/vacuum/.translations/es.json b/homeassistant/components/vacuum/.translations/es.json new file mode 100644 index 00000000000..9ecf3ade99c --- /dev/null +++ b/homeassistant/components/vacuum/.translations/es.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Deje que {entity_name} limpie", + "dock": "Deje que {entity_name} regrese a la base" + }, + "condtion_type": { + "is_cleaning": "{entity_name} est\u00e1 limpiando", + "is_docked": "{entity_name} est\u00e1 acoplado" + }, + "trigger_type": { + "cleaning": "{entity_name} empez\u00f3 a limpiar", + "docked": "{entity_name} en la base" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json index ee0cf523588..c1e969c7f51 100644 --- a/homeassistant/components/withings/.translations/es.json +++ b/homeassistant/components/withings/.translations/es.json @@ -7,6 +7,13 @@ "default": "Autenticado correctamente con Withings para el perfil seleccionado." }, "step": { + "profile": { + "data": { + "profile": "Perfil" + }, + "description": "\u00bfQu\u00e9 perfil seleccion\u00f3 en el sitio web de Withings? Es importante que los perfiles coincidan, de lo contrario los datos se etiquetar\u00e1n incorrectamente.", + "title": "Perfil de usuario." + }, "user": { "data": { "profile": "Perfil" diff --git a/homeassistant/components/wled/.translations/es.json b/homeassistant/components/wled/.translations/es.json new file mode 100644 index 00000000000..7dd388d41af --- /dev/null +++ b/homeassistant/components/wled/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", + "connection_error": "No se ha podido conectar al dispositivo WLED." + }, + "error": { + "connection_error": "No se ha podido conectar al dispositivo WLED." + }, + "flow_title": "WLED: {nombre}", + "step": { + "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, + "description": "Configure su WLED para integrarse con Home Assistant.", + "title": "Vincula tu WLED" + }, + "zeroconf_confirm": { + "description": "\u00bfQuieres a\u00f1adir el WLED llamado `{name}` a Home Assistant?", + "title": "Descubierto dispositivo WLED" + } + }, + "title": "WLED" + } +} \ No newline at end of file From 70e8c582541fa91b6845325be6656f999579cf7b Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 13 Nov 2019 07:42:12 +0100 Subject: [PATCH 274/306] version bump pypoint (#28737) --- homeassistant/components/point/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 0f2faef86df..4c29f37e67c 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/point", "requirements": [ - "pypoint==1.1.1" + "pypoint==1.1.2" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index 163918f5180..97e34b09fa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1411,7 +1411,7 @@ pypck==0.6.3 pypjlink2==1.2.0 # homeassistant.components.point -pypoint==1.1.1 +pypoint==1.1.2 # homeassistant.components.ps4 pyps4-2ndscreen==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90668e3eaab..1dcb0323c79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -468,7 +468,7 @@ pyotgw==0.5b0 pyotp==2.3.0 # homeassistant.components.point -pypoint==1.1.1 +pypoint==1.1.2 # homeassistant.components.ps4 pyps4-2ndscreen==1.0.1 From d7f45a47f5d53393f8c5a5668f3194f14a60a39d Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 13 Nov 2019 07:43:38 +0100 Subject: [PATCH 275/306] Upgrade async_upnp_client==0.14.12 (#28733) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 008a1293e41..ac09b1afb48 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "Dlna dmr", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": [ - "async-upnp-client==0.14.11" + "async-upnp-client==0.14.12" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index d4446b271f9..a78fec3610e 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": [ - "async-upnp-client==0.14.11" + "async-upnp-client==0.14.12" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 97e34b09fa3..5b4f0358b0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.upnp -async-upnp-client==0.14.11 +async-upnp-client==0.14.12 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1dcb0323c79..f891394db6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ arcam-fmj==0.4.3 # homeassistant.components.dlna_dmr # homeassistant.components.upnp -async-upnp-client==0.14.11 +async-upnp-client==0.14.12 # homeassistant.components.stream av==6.1.2 From 4bcc669d19521d061f0f2db28a366f0a0065ac83 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 13 Nov 2019 07:42:46 +0000 Subject: [PATCH 276/306] Add device classes to weather sensors. (#28512) --- homeassistant/components/yr/sensor.py | 43 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index f562f519ab5..fc754c9a257 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -19,6 +19,11 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION, CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_HPA, + TEMP_CELSIUS, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity @@ -34,20 +39,24 @@ ATTRIBUTION = ( # https://api.met.no/license_data.html SENSOR_TYPES = { - "symbol": ["Symbol", None], - "precipitation": ["Precipitation", "mm"], - "temperature": ["Temperature", "°C"], - "windSpeed": ["Wind speed", "m/s"], - "windGust": ["Wind gust", "m/s"], - "pressure": ["Pressure", "hPa"], - "windDirection": ["Wind direction", "°"], - "humidity": ["Humidity", "%"], - "fog": ["Fog", "%"], - "cloudiness": ["Cloudiness", "%"], - "lowClouds": ["Low clouds", "%"], - "mediumClouds": ["Medium clouds", "%"], - "highClouds": ["High clouds", "%"], - "dewpointTemperature": ["Dewpoint temperature", "°C"], + "symbol": ["Symbol", None, None], + "precipitation": ["Precipitation", "mm", None], + "temperature": ["Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], + "windSpeed": ["Wind speed", "m/s", None], + "windGust": ["Wind gust", "m/s", None], + "pressure": ["Pressure", PRESSURE_HPA, DEVICE_CLASS_PRESSURE], + "windDirection": ["Wind direction", "°", None], + "humidity": ["Humidity", "%", DEVICE_CLASS_HUMIDITY], + "fog": ["Fog", "%", None], + "cloudiness": ["Cloudiness", "%", None], + "lowClouds": ["Low clouds", "%", None], + "mediumClouds": ["Medium clouds", "%", None], + "highClouds": ["High clouds", "%", None], + "dewpointTemperature": [ + "Dewpoint temperature", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + ], } CONF_FORECAST = "forecast" @@ -103,6 +112,7 @@ class YrSensor(Entity): self.type = sensor_type self._state = None self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._device_class = SENSOR_TYPES[self.type][2] @property def name(self): @@ -139,6 +149,11 @@ class YrSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def device_class(self): + """Return the device class of this entity, if any.""" + return self._device_class + class YrData: """Get the latest data and updates the states.""" From b4cec23adde3d7f90f4670f3bcae1176d2949fac Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 13 Nov 2019 08:43:14 +0100 Subject: [PATCH 277/306] Upgrade psutil to 5.6.5 (#28717) --- homeassistant/components/systemmonitor/manifest.json | 2 +- homeassistant/components/systemmonitor/sensor.py | 5 ++--- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index a45a59e410a..67677bc2572 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Systemmonitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ - "psutil==5.6.3" + "psutil==5.6.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 53c5c104cd1..b1a33736083 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -7,12 +7,11 @@ import psutil import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_RESOURCES, STATE_OFF, STATE_ON, CONF_TYPE -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_RESOURCES, CONF_TYPE, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5b4f0358b0a..68ac061815d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1020,7 +1020,7 @@ prometheus_client==0.7.1 protobuf==3.6.1 # homeassistant.components.systemmonitor -psutil==5.6.3 +psutil==5.6.5 # homeassistant.components.ptvsd ptvsd==4.2.8 From a48d426f182ec859f2ea9f105e921cfa5528193b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 13 Nov 2019 09:50:35 +0200 Subject: [PATCH 278/306] Travis and tox config improvements (#28667) * Use travis_wait only with pylint pylint is the only job that is expected to be silent for extended time. For others such a silence is a sign of a problem and using travis_wait just lengthens the wait, and makes things harder to follow and debug, because it also suppresses output in the web UI. * Use pytest-xdist in tox Similarly as in Azure. --- .travis.yml | 4 ++-- tox.ini | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6d5b43c2f03..c9638b02a2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: - python: "3.6.1" env: TOXENV=lint - python: "3.6.1" - env: TOXENV=pylint PYLINT_ARGS=--jobs=0 + env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30 - python: "3.6.1" env: TOXENV=typing - python: "3.6.1" @@ -33,4 +33,4 @@ cache: - $HOME/.cache/pre-commit install: pip install -U tox language: python -script: travis_wait 50 tox --develop +script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox --develop diff --git a/tox.ini b/tox.ini index 2898c410b2a..dc2a9f79b90 100644 --- a/tox.ini +++ b/tox.ini @@ -5,19 +5,21 @@ skip_missing_interpreters = True [testenv] basepython = {env:PYTHON3_PATH:python3} commands = - pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar {posargs} + pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt + pytest-xdist [testenv:cov] commands = - pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} + pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt + pytest-xdist [testenv:pylint] ignore_errors = True From fe942c40a0b4806525da9749d3fd04e34b188f57 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 14 Nov 2019 00:11:09 +1300 Subject: [PATCH 279/306] Correct openalpr_local config option name (#28746) Previously was "alp_bin" now "alpr_bin" like it is outlined in the documentation (https://www.home-assistant.io/integrations/openalpr_local/) --- homeassistant/components/openalpr_local/image_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index 786f5ca69da..32a08b53165 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -47,7 +47,7 @@ OPENALPR_REGIONS = [ "vn2", ] -CONF_ALPR_BIN = "alp_bin" +CONF_ALPR_BIN = "alpr_bin" DEFAULT_BINARY = "alpr" From 349e7b8cd178dac090e6f5a9948afef5137bcb6b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Nov 2019 13:55:02 +0100 Subject: [PATCH 280/306] Bumped version to 0.102.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 449e7a90087..fa7f753495c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 4f089aba356fbb76444622a736fc923aa7bf4fdc Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 13 Nov 2019 09:25:45 -0500 Subject: [PATCH 281/306] Bump ZHA quirks to 0.0.28 (#28750) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 18e8af7008d..8781625d326 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.11.0", - "zha-quirks==0.0.27", + "zha-quirks==0.0.28", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.11.0", "zigpy-xbee-homeassistant==0.7.0", diff --git a/requirements_all.txt b/requirements_all.txt index 68ac061815d..9043f944d42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2054,7 +2054,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.27 +zha-quirks==0.0.28 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f891394db6c..bd57a5490a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -637,7 +637,7 @@ yahooweather==0.10 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.27 +zha-quirks==0.0.28 # homeassistant.components.zha zigpy-deconz==0.7.0 From 520e4296babd17f9005843581c7a981be4fda681 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 14 Nov 2019 14:12:46 +0100 Subject: [PATCH 282/306] Updated frontend to 20191114.0 (#28768) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 8f7212209cf..b7506f599ef 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191108.0" + "home-assistant-frontend==20191114.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ade3305edc9..dfe53ed2e19 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9043f944d42..058ad143f6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd57a5490a3..969107d3707 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From af2443fb10b8e028d705701eb5623a1f1b6daa86 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Nov 2019 14:34:13 +0100 Subject: [PATCH 283/306] Fix account link version check (#28770) --- homeassistant/components/cloud/account_link.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 6fbfcc8723b..9ec1fe634d7 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -13,7 +13,6 @@ from .const import DOMAIN DATA_SERVICES = "cloud_account_link_services" CACHE_TIMEOUT = 3600 -PATCH_VERSION = int(PATCH_VERSION.split(".")[0]) _LOGGER = logging.getLogger(__name__) @@ -49,7 +48,20 @@ def _is_older(version: str) -> bool: except ValueError: return False - cur_version_parts = [MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION] + patch_number_str = "" + + for char in PATCH_VERSION: + if char.isnumeric(): + patch_number_str += char + else: + break + + try: + patch_number = int(patch_number_str) + except ValueError: + patch_number = 0 + + cur_version_parts = [MAJOR_VERSION, MINOR_VERSION, patch_number] return version_parts <= cur_version_parts From f5a9bcdf6d42b31db3767b2946add88333cc2bbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Nov 2019 14:36:30 +0100 Subject: [PATCH 284/306] Bumped version to 0.102.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fa7f753495c..620af2b79db 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 326c25a7660ec758dc13aaafbaa0ada822f18b4c Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Thu, 14 Nov 2019 12:07:43 -0800 Subject: [PATCH 285/306] Fix amazon dependency conflicts (#28217) * fix amazon dependency conflicts * bump boto3 for route53 --- homeassistant/components/amazon_polly/manifest.json | 4 ++-- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/route53/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 45e382647f8..c07aad079e4 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -3,10 +3,10 @@ "name": "Amazon polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": [ - "boto3==1.9.233" + "boto3==1.9.252" ], "dependencies": [], "codeowners": [ "@robbiet480" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index a4543cc4b0f..b617eb75ee1 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -3,7 +3,7 @@ "name": "Aws", "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": [ - "aiobotocore==0.10.2" + "aiobotocore==0.10.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 34a296b0f9d..307132aa01b 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -3,7 +3,7 @@ "name": "Route53", "documentation": "https://www.home-assistant.io/integrations/route53", "requirements": [ - "boto3==1.9.233", + "boto3==1.9.252", "ipify==1.0.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 058ad143f6f..65fe34ae7f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -136,7 +136,7 @@ aioasuswrt==1.1.22 aioautomatic==0.6.5 # homeassistant.components.aws -aiobotocore==0.10.2 +aiobotocore==0.10.4 # homeassistant.components.dnsip aiodns==2.0.0 @@ -317,7 +317,7 @@ bomradarloop==0.1.3 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.9.233 +boto3==1.9.252 # homeassistant.components.braviatv braviarc-homeassistant==0.3.7.dev0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 969107d3707..582d0690168 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -47,7 +47,7 @@ aioasuswrt==1.1.22 aioautomatic==0.6.5 # homeassistant.components.aws -aiobotocore==0.10.2 +aiobotocore==0.10.4 # homeassistant.components.esphome aioesphomeapi==2.5.0 From 1479e7353ba24464b0d238f757f361f6acba60cf Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Fri, 15 Nov 2019 09:21:46 +0100 Subject: [PATCH 286/306] Change unique id for SAJ sensor based on device SN (#28663) * Change unique id for SAJ sensor based on device SN * Add SAJ device name + sn to state attributes * Revert device state attributes (after review) --- homeassistant/components/saj/manifest.json | 2 +- homeassistant/components/saj/sensor.py | 21 +++++++++++---------- requirements_all.txt | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index 4d02ab74840..02d83916d50 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.13" + "pysaj==0.0.14" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 7542440c102..2a17d110c6e 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -69,9 +69,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Use all sensors by default hass_sensors = [] - for sensor in sensor_def: - hass_sensors.append(SAJsensor(sensor, inverter_name=config.get(CONF_NAME))) - kwargs = {} if wifi: kwargs["wifi"] = True @@ -81,7 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: saj = pysaj.SAJ(config[CONF_HOST], **kwargs) - await saj.read(sensor_def) + done = await saj.read(sensor_def) except pysaj.UnauthorizedException: _LOGGER.error("Username and/or password is wrong.") return @@ -91,7 +88,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) return - async_add_entities(hass_sensors) + if done: + for sensor in sensor_def: + hass_sensors.append( + SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) + ) + + async_add_entities(hass_sensors) async def async_saj(): """Update all the SAJ sensors.""" @@ -163,10 +166,11 @@ def async_track_time_interval_backoff(hass, action) -> CALLBACK_TYPE: class SAJsensor(Entity): """Representation of a SAJ sensor.""" - def __init__(self, pysaj_sensor, inverter_name=None): + def __init__(self, serialnumber, pysaj_sensor, inverter_name=None): """Initialize the sensor.""" self._sensor = pysaj_sensor self._inverter_name = inverter_name + self._serialnumber = serialnumber self._state = self._sensor.value @property @@ -235,7 +239,4 @@ class SAJsensor(Entity): @property def unique_id(self): """Return a unique identifier for this sensor.""" - if self._inverter_name: - return f"{self._inverter_name}_{self._sensor.name}" - - return f"{self._sensor.name}" + return f"{self._serialnumber}_{self._sensor.name}" diff --git a/requirements_all.txt b/requirements_all.txt index 65fe34ae7f5..eb638d9767e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1435,7 +1435,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.13 +pysaj==0.0.14 # homeassistant.components.sony_projector pysdcp==1 From 5f0f5ca5574f6ec824f3191da0ff25aa14b036a1 Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Fri, 15 Nov 2019 11:49:56 +0000 Subject: [PATCH 287/306] Fix changing venstar operation_mode (#28754) --- homeassistant/components/venstar/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index c948772197f..de26d236649 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -278,7 +278,7 @@ class VenstarThermostat(ClimateDevice): temperature = kwargs.get(ATTR_TEMPERATURE) if operation_mode and self._mode_map.get(operation_mode) != self._client.mode: - set_temp = self._set_operation_mode(self._mode_map.get(operation_mode)) + set_temp = self._set_operation_mode(operation_mode) if set_temp: if ( From fe5f30ba7887db0c2a9a53b3b28591dfcae75a79 Mon Sep 17 00:00:00 2001 From: Morten Trab Date: Sat, 16 Nov 2019 09:45:43 +0100 Subject: [PATCH 288/306] Fix Repetier integration entity indexing (#28766) * Fixed multi extruder/beds/chambers index issue, #28130 * Switched from .format to f style name formatting * Fixed incorrect indexing * Removed VS files * Removed not need temp_id subtraction * Removed VS files * Fixed access mode * Fixed access mode * Fixing access mode - again --- homeassistant/components/repetier/__init__.py | 8 ++++---- homeassistant/components/repetier/sensor.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 12975baca91..1d6026a8754 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -108,7 +108,7 @@ def has_all_unique_names(value): SENSOR_TYPES = { - # Type, Unit, Icon + # Type, Unit, Icon, post "bed_temperature": ["temperature", TEMP_CELSIUS, "mdi:thermometer", "_bed_"], "extruder_temperature": [ "temperature", @@ -248,12 +248,12 @@ class PrinterAPI: if prop_data is None: continue for idx, _ in enumerate(prop_data): - info["temp_id"] = idx - sensor_info.append(info) + prop_info = info.copy() + prop_info["temp_id"] = idx + sensor_info.append(prop_info) else: info["temp_id"] = None sensor_info.append(info) - self._known_entities.add(known) if not sensor_info: diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index e692ffc078f..5936b5c3343 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -35,11 +35,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): printer_id = info["printer_id"] sensor_type = info["sensor_type"] temp_id = info["temp_id"] - name = info["name"] + name = f"{info['name']}{SENSOR_TYPES[sensor_type][3]}" if temp_id is not None: - name = "{}{}{}".format(name, SENSOR_TYPES[sensor_type][3], temp_id) - else: - name = "{}{}".format(name, SENSOR_TYPES[sensor_type][3]) + _LOGGER.debug("%s Temp_id: %s", sensor_type, temp_id) + name = f"{name}{temp_id}" sensor_class = sensor_map[sensor_type] entity = sensor_class(api, temp_id, name, printer_id, sensor_type) entities.append(entity) From 5ff24ecf77733c3bfa12c0b83629ae512762c0d8 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 15 Nov 2019 09:55:40 +0100 Subject: [PATCH 289/306] Fix HomematicIP Cloud Alarm Control Panel support for basic mode (#28778) --- .../homematicip_cloud/alarm_control_panel.py | 67 +++-------------- .../test_alarm_control_panel.py | 72 +++++-------------- .../homematicip_cloud/test_climate.py | 1 + .../components/homematicip_cloud/test_init.py | 4 +- 4 files changed, 31 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index a7b1beaec93..8ebb35b12c1 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,8 +1,7 @@ """Support for HomematicIP Cloud alarm control panel.""" import logging -from homematicip.aio.group import AsyncSecurityZoneGroup -from homematicip.base.enums import WindowState +from homematicip.functionalHomes import SecurityAndAlarmHome from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.config_entries import ConfigEntry @@ -32,34 +31,15 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] - security_zones = [] - for group in hap.home.groups: - if isinstance(group, AsyncSecurityZoneGroup): - security_zones.append(group) - - if security_zones: - devices.append(HomematicipAlarmControlPanel(hap, security_zones)) - - if devices: - async_add_entities(devices) + async_add_entities([HomematicipAlarmControlPanel(hap)]) class HomematicipAlarmControlPanel(AlarmControlPanel): """Representation of an alarm control panel.""" - def __init__(self, hap: HomematicipHAP, security_zones) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the alarm control panel.""" self._home = hap.home - self.alarm_state = STATE_ALARM_DISARMED - self._internal_alarm_zone = None - self._external_alarm_zone = None - - for security_zone in security_zones: - if security_zone.label == "INTERNAL": - self._internal_alarm_zone = security_zone - elif security_zone.label == "EXTERNAL": - self._external_alarm_zone = security_zone @property def device_info(self): @@ -75,28 +55,23 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): @property def state(self) -> str: """Return the state of the device.""" + # check for triggered alarm + if self._security_and_alarm.alarmActive: + return STATE_ALARM_TRIGGERED + activation_state = self._home.get_security_zones_activation() # check arm_away if activation_state == (True, True): - if self._internal_alarm_zone_state or self._external_alarm_zone_state: - return STATE_ALARM_TRIGGERED return STATE_ALARM_ARMED_AWAY # check arm_home if activation_state == (False, True): - if self._external_alarm_zone_state: - return STATE_ALARM_TRIGGERED return STATE_ALARM_ARMED_HOME return STATE_ALARM_DISARMED @property - def _internal_alarm_zone_state(self) -> bool: - return _get_zone_alarm_state(self._internal_alarm_zone) - - @property - def _external_alarm_zone_state(self) -> bool: - """Return the state of the device.""" - return _get_zone_alarm_state(self._external_alarm_zone) + def _security_and_alarm(self): + return self._home.get_functionalHome(SecurityAndAlarmHome) async def async_alarm_disarm(self, code=None): """Send disarm command.""" @@ -112,10 +87,7 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): async def async_added_to_hass(self): """Register callbacks.""" - if self._internal_alarm_zone: - self._internal_alarm_zone.on_update(self._async_device_changed) - if self._external_alarm_zone: - self._external_alarm_zone.on_update(self._async_device_changed) + self._home.on_update(self._async_device_changed) def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" @@ -138,26 +110,9 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): @property def available(self) -> bool: """Device available.""" - return ( - not self._internal_alarm_zone.unreach - or not self._external_alarm_zone.unreach - ) + return self._home.connected @property def unique_id(self) -> str: """Return a unique ID.""" return f"{self.__class__.__name__}_{self._home.id}" - - -def _get_zone_alarm_state(security_zone) -> bool: - if security_zone and security_zone.active: - if ( - security_zone.sabotage - or security_zone.motionDetected - or security_zone.presenceDetected - or security_zone.windowState == WindowState.OPEN - or security_zone.windowState == WindowState.TILTED - ): - return True - - return False diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 2798a0879b7..78bc0a09ea5 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -1,7 +1,4 @@ """Tests for HomematicIP Cloud alarm control panel.""" -from homematicip.base.enums import WindowState -from homematicip.group import SecurityZoneGroup - from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) @@ -17,29 +14,24 @@ from homeassistant.setup import async_setup_component from .helper import get_and_check_entity_basics -def _get_security_zones(groups): # pylint: disable=W0221 - """Get the security zones.""" - for group in groups: - if isinstance(group, SecurityZoneGroup): - if group.label == "EXTERNAL": - external = group - elif group.label == "INTERNAL": - internal = group - return internal, external - - async def _async_manipulate_security_zones( - hass, home, internal_active, external_active, window_state + hass, home, internal_active=False, external_active=False, alarm_triggered=False ): """Set new values on hmip security zones.""" - internal_zone, external_zone = _get_security_zones(home.groups) + json = home._rawJSONData # pylint: disable=W0212 + json["functionalHomes"]["SECURITY_AND_ALARM"]["alarmActive"] = alarm_triggered + external_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ + "EXTERNAL" + ] + internal_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ + "INTERNAL" + ] + external_zone = home.search_group_by_id(external_zone_id) external_zone.active = external_active - external_zone.windowState = window_state + internal_zone = home.search_group_by_id(internal_zone_id) internal_zone.active = internal_active - # Just one call to a security zone is required to refresh the ACP. - internal_zone.fire_update_event() - + home.fire_update_event(json) await hass.async_block_till_done() @@ -70,79 +62,49 @@ async def test_hmip_alarm_control_panel(hass, default_mock_hap): assert not hmip_device home = default_mock_hap.home - service_call_counter = len(home.mock_calls) await hass.services.async_call( "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 1 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (True, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=True, - external_active=True, - window_state=WindowState.CLOSED, + hass, home, internal_active=True, external_active=True ) assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_AWAY await hass.services.async_call( "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 3 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, True) - await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=True, - window_state=WindowState.CLOSED, - ) + await _async_manipulate_security_zones(hass, home, external_active=True) assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_HOME await hass.services.async_call( "alarm_control_panel", "alarm_disarm", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 5 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, False) - await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=False, - window_state=WindowState.CLOSED, - ) + await _async_manipulate_security_zones(hass, home) assert hass.states.get(entity_id).state is STATE_ALARM_DISARMED await hass.services.async_call( "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 7 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (True, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=True, - external_active=True, - window_state=WindowState.OPEN, + hass, home, internal_active=True, external_active=True, alarm_triggered=True ) assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED await hass.services.async_call( "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 9 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=True, - window_state=WindowState.OPEN, + hass, home, external_active=True, alarm_triggered=True ) assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 858fba29563..2b233a6dee2 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -343,6 +343,7 @@ async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap): hass, default_mock_hap, entity_id, entity_name, device_model ) + assert hmip_device assert ha_state.state == HVAC_MODE_AUTO assert ha_state.attributes["current_temperature"] == 24.7 assert ha_state.attributes["min_temp"] == 5.0 diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index ba27a619e6a..eb51c3ece38 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -161,5 +161,5 @@ async def test_hmip_dump_hap_config_services(hass, mock_hap_with_service): ) home = mock_hap_with_service.home assert home.mock_calls[-1][0] == "download_configuration" - assert len(home.mock_calls) == 8 # pylint: disable=W0212 - assert len(write_mock.mock_calls) > 0 + assert home.mock_calls + assert write_mock.mock_calls From f8be1512b80c5dea23d1c576b25050d0b4553f1a Mon Sep 17 00:00:00 2001 From: LeoCal <25389602+LeoCal@users.noreply.github.com> Date: Fri, 15 Nov 2019 10:52:15 +0100 Subject: [PATCH 290/306] Fix Swisscom empty response received (#28782) --- homeassistant/components/swisscom/device_tracker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index adb018a4b4b..5662212c9e8 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -92,6 +92,10 @@ class SwisscomDeviceScanner(DeviceScanner): _LOGGER.info("No response from Swisscom Internet Box") return devices + if "status" not in request.json(): + _LOGGER.info("No status in response from Swisscom Internet Box") + return devices + for device in request.json()["status"]: try: devices[device["Key"]] = { From 31cbdbf1dddc25377b8a9afe4c437539ae9d0c17 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Sat, 16 Nov 2019 10:37:58 +0100 Subject: [PATCH 291/306] Fix broken postnl sensor (#28794) * fix broken postnl sensor * make sure shipment list is not growing indefinitely --- homeassistant/components/postnl/manifest.json | 2 +- homeassistant/components/postnl/sensor.py | 11 ++++++++--- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/postnl/manifest.json b/homeassistant/components/postnl/manifest.json index d07f9746ee8..c45eea0610d 100644 --- a/homeassistant/components/postnl/manifest.json +++ b/homeassistant/components/postnl/manifest.json @@ -3,7 +3,7 @@ "name": "Postnl", "documentation": "https://www.home-assistant.io/integrations/postnl", "requirements": [ - "postnl_api==1.0.2" + "postnl_api==1.2.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/postnl/sensor.py b/homeassistant/components/postnl/sensor.py index cd190c09d87..6155f58519a 100644 --- a/homeassistant/components/postnl/sensor.py +++ b/homeassistant/components/postnl/sensor.py @@ -58,7 +58,7 @@ class PostNLSensor(Entity): def __init__(self, api, name): """Initialize the PostNL sensor.""" self._name = name - self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION, "shipments": []} self._state = None self._api = api @@ -90,6 +90,11 @@ class PostNLSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update device state.""" - shipments = self._api.get_relevant_shipments() - self._attributes["shipments"] = shipments + shipments = self._api.get_relevant_deliveries() + + self._attributes["shipments"] = [] + + for shipment in shipments: + self._attributes["shipments"].append(vars(shipment)) + self._state = len(shipments) diff --git a/requirements_all.txt b/requirements_all.txt index eb638d9767e..607574b4cfa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -999,7 +999,7 @@ pmsensor==0.4 pocketcasts==0.1 # homeassistant.components.postnl -postnl_api==1.0.2 +postnl_api==1.2.2 # homeassistant.components.reddit praw==6.4.0 From b964fcc5b1d5e467ee81da8de0996912116dae11 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 16 Nov 2019 07:22:06 +0100 Subject: [PATCH 292/306] Updated frontend to 20191115.0 (#28797) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b7506f599ef..6c59ea38fe4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191114.0" + "home-assistant-frontend==20191115.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dfe53ed2e19..3688d6dfa38 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 607574b4cfa..6f706edc740 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 582d0690168..c42188c920f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From c6343c9e88ce55b719b544cb647a488849086be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sat, 16 Nov 2019 15:05:17 +0100 Subject: [PATCH 293/306] Fix Comfoconnect errors during startup (#28802) * Add callback registrations to async_added_to_hass * Fix CODEOWNERS * Fix code formatting * Requested changes. * Don't pass unused hass and fix string formatting * Fix import order. --- CODEOWNERS | 1 + .../components/comfoconnect/__init__.py | 5 --- homeassistant/components/comfoconnect/fan.py | 35 ++++++++++-------- .../components/comfoconnect/manifest.json | 2 +- .../components/comfoconnect/sensor.py | 36 +++++++++++-------- 5 files changed, 45 insertions(+), 34 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 879a1c8f55d..ec92d918679 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -61,6 +61,7 @@ homeassistant/components/cisco_webex_teams/* @fbradyirl homeassistant/components/ciscospark/* @fbradyirl homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus +homeassistant/components/comfoconnect/* @michaelarnauts homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core homeassistant/components/conversation/* @home-assistant/core diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index aef4bf1deeb..efdbf020f1a 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -102,7 +102,6 @@ class ComfoConnectBridge: def __init__(self, hass, bridge, name, token, friendly_name, pin): """Initialize the ComfoConnect bridge.""" - self.data = {} self.name = name self.hass = hass @@ -136,7 +135,3 @@ class ComfoConnectBridge: # Notify listeners that we have received an update dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var) - - def subscribe_sensor(self, sensor_id): - """Subscribe for the specified sensor.""" - self.comfoconnect.register_sensor(sensor_id) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index bbb4b0176bf..34e784d61eb 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -17,7 +17,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge @@ -30,28 +30,36 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ComfoConnect fan platform.""" ccb = hass.data[DOMAIN] - add_entities([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True) + add_entities([ComfoConnectFan(ccb.name, ccb)], True) class ComfoConnectFan(FanEntity): """Representation of the ComfoConnect fan platform.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: + def __init__(self, name, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" - self._ccb = ccb self._name = name - # Ask the bridge to keep us updated - self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE) + async def async_added_to_hass(self): + """Register for sensor updates.""" + await self.hass.async_add_executor_job( + self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE + ) + async_dispatcher_connect( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update + ) - def _handle_update(var): - if var == SENSOR_FAN_SPEED_MODE: - _LOGGER.debug("Dispatcher update for %s", var) - self.schedule_update_ha_state() + def _handle_update(self, var): + """Handle update callbacks.""" + if var == SENSOR_FAN_SPEED_MODE: + _LOGGER.debug("Received update for %s", var) + self.schedule_update_ha_state() - # Register for dispatcher updates - dispatcher_connect(hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False @property def name(self): @@ -71,7 +79,6 @@ class ComfoConnectFan(FanEntity): @property def speed(self): """Return the current fan mode.""" - try: speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] return SPEED_MAPPING[speed] @@ -95,7 +102,7 @@ class ComfoConnectFan(FanEntity): def set_speed(self, speed: str): """Set fan speed.""" - _LOGGER.debug("Changing fan speed to %s.", speed) + _LOGGER.debug("Changing fan speed to %s", speed) if speed == SPEED_OFF: self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 57daba7fdbd..091b7f7bcdd 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -6,5 +6,5 @@ "pycomfoconnect==0.3" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@michaelarnauts"] } diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 06d0506e2cf..a1f16ed9631 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -11,7 +11,7 @@ from pycomfoconnect import ( ) from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from . import ( @@ -81,13 +81,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_type = resource.lower() if sensor_type not in SENSOR_TYPES: - _LOGGER.warning("Sensor type: %s is not a valid sensor.", sensor_type) + _LOGGER.warning("Sensor type: %s is not a valid sensor", sensor_type) continue sensors.append( ComfoConnectSensor( - hass, - name="%s %s" % (ccb.name, SENSOR_TYPES[sensor_type][0]), + name=f"{ccb.name} {SENSOR_TYPES[sensor_type][0]}", ccb=ccb, sensor_type=sensor_type, ) @@ -99,23 +98,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ComfoConnectSensor(Entity): """Representation of a ComfoConnect sensor.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge, sensor_type) -> None: + def __init__(self, name, ccb: ComfoConnectBridge, sensor_type) -> None: """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type self._sensor_id = SENSOR_TYPES[self._sensor_type][3] self._name = name - # Register the requested sensor - self._ccb.comfoconnect.register_sensor(self._sensor_id) + async def async_added_to_hass(self): + """Register for sensor updates.""" + await self.hass.async_add_executor_job( + self._ccb.comfoconnect.register_sensor, self._sensor_id + ) + async_dispatcher_connect( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update + ) - def _handle_update(var): - if var == self._sensor_id: - _LOGGER.debug("Dispatcher update for %s.", var) - self.schedule_update_ha_state() - - # Register for dispatcher updates - dispatcher_connect(hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + def _handle_update(self, var): + """Handle update callbacks.""" + if var == self._sensor_id: + _LOGGER.debug("Received update for %s", var) + self.schedule_update_ha_state() @property def state(self): @@ -125,6 +128,11 @@ class ComfoConnectSensor(Entity): except KeyError: return None + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + @property def name(self): """Return the name of the sensor.""" From fb1fd19aae800f67c2a08a8003ce48e72554425c Mon Sep 17 00:00:00 2001 From: Jackie Yang Date: Sun, 17 Nov 2019 03:18:53 -0800 Subject: [PATCH 294/306] Fix miio air quality sensor (#28828) Fix https://github.com/home-assistant/home-assistant/issues/28827 --- homeassistant/components/xiaomi_miio/air_quality.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index b80906aa0cb..3824c5b88cd 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -54,7 +54,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_info.firmware_version, device_info.hardware_version, ) - device = AirMonitorB1(name, AirQualityMonitor(host, token, model), unique_id) + device = AirMonitorB1(name, AirQualityMonitor(host, token, model=model), unique_id) async_add_entities([device], update_before_add=True) From d91eddc4f05fc4141c41de5ea0501dcb05f947d7 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 18 Nov 2019 09:10:59 +0100 Subject: [PATCH 295/306] Update pyatmo to 3.0.1 (#28829) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index f6c08faf8fa..9d1178d9d17 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==2.3.3" + "pyatmo==3.0.1" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index 6f706edc740..4ca7c2ae0f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.3.3 +pyatmo==3.0.1 # homeassistant.components.atome pyatome==0.1.1 From bf4c81aa5e61b34011aca27d9cf9b4f212938dbe Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 18 Nov 2019 15:40:26 +0100 Subject: [PATCH 296/306] Updated frontend to 20191118.0 (#28852) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6c59ea38fe4..51906a100cc 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191115.0" + "home-assistant-frontend==20191118.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3688d6dfa38..b4ec45e9fdb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4ca7c2ae0f9..24ab0ce339a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c42188c920f..8c8829308cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 7966411274e1d9e7f7be3b7530fa985738f5b166 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 18 Nov 2019 15:44:17 +0100 Subject: [PATCH 297/306] Bump version --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 620af2b79db..0bc6be37dc7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 89cd5d46cdb09f75bf655148e460170d09f4eddc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Nov 2019 18:33:09 +0100 Subject: [PATCH 298/306] Fix documentation URL in failed platform config check (#28814) * Fix documentation URL in failed platform config check * Replace pop from list by access using negative index * Use of split instead of rsplit --- homeassistant/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 864ced6a16a..e6be2b9c7a5 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -456,9 +456,10 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: ) if domain != CONF_CORE: + integration = domain.split(".")[-1] message += ( "Please check the docs at " - "https://home-assistant.io/integrations/{}/".format(domain) + f"https://home-assistant.io/integrations/{integration}/" ) return message From 687b7fc8cbd131bde15452b2277070a514e30786 Mon Sep 17 00:00:00 2001 From: Andi Date: Mon, 18 Nov 2019 17:03:10 +0100 Subject: [PATCH 299/306] Fix Synology camera whitelist (#28822) * Fix Synology camera whitelist If whitelist config is set, not camera is added to HA at all. * Fix Synology Camera whitelist Fix typo in config key. * Update camera.py Access config dict the voluptuous way --- homeassistant/components/synology/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 8c176f48803..91ee5a98fc3 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -62,7 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # add cameras devices = [] for camera in cameras: - if not config.get(CONF_WHITELIST): + if not config[CONF_WHITELIST] or camera.name in config[CONF_WHITELIST]: device = SynologyCamera(surveillance, camera.camera_id, verify_ssl) devices.append(device) From ab413108479fabfca76af06e1773e4a274462c83 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 19 Nov 2019 23:16:59 +0100 Subject: [PATCH 300/306] Fix setting colors while reproducing a lights state (#28871) * Fix setting colors while reproducing state * Reorder list --- homeassistant/components/light/reproduce_state.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index c84b3627bed..90d14c2a19f 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -45,13 +45,14 @@ ATTR_GROUP = [ ] COLOR_GROUP = [ - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_HS_COLOR, - ATTR_KELVIN, - ATTR_PROFILE, + ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_XY_COLOR, + # The following color attributes are deprecated + ATTR_PROFILE, + ATTR_COLOR_NAME, + ATTR_KELVIN, ] DEPRECATED_GROUP = [ From 69b096023a33addd5c321978ecc09a3cc28f595a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 19 Nov 2019 14:48:51 +0100 Subject: [PATCH 301/306] Updated frontend to 20191119.0 (#28875) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 51906a100cc..d44ddb60679 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191118.0" + "home-assistant-frontend==20191119.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b4ec45e9fdb..a13648d2a3b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 24ab0ce339a..13470f4b8f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c8829308cf..132fddb58ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 9fb289ad93cb7221b24f7a6514b4e200fa805ca8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 19 Nov 2019 23:16:43 +0100 Subject: [PATCH 302/306] Updated frontend to 20191119.1 (#28881) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index d44ddb60679..cc7722724ea 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.0" + "home-assistant-frontend==20191119.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a13648d2a3b..f3939120124 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 13470f4b8f6..9790b3c9974 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 132fddb58ba..9f905160ebc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 98c7ddc0cf66e7aebcc17b59f1adfd2035cd0cf4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 20 Nov 2019 12:30:32 +0100 Subject: [PATCH 303/306] Updated frontend to 20191119.2 (#28896) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cc7722724ea..6e513d55742 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.1" + "home-assistant-frontend==20191119.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f3939120124..a98bb2b46d7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9790b3c9974..833a4d47105 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f905160ebc..75564e13b94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From bca93ca2ec6c3adfe50b4a75a41f7173cb11a1cf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 20 Nov 2019 12:33:39 +0100 Subject: [PATCH 304/306] Bumped version to 0.102.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0bc6be37dc7..cb3b234595c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 787aac7cf2a075996005701d177e3845c025e201 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 20 Nov 2019 22:03:01 +0100 Subject: [PATCH 305/306] Fix Almond onboarding url when using cloud (#28908) --- homeassistant/components/almond/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 6d4ab31bf17..7c1f65f3ac3 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -263,8 +263,6 @@ class AlmondAgent(conversation.AbstractConversationAgent): host = self.entry.data["host"] if self.entry.data.get("is_hassio"): host = "/core_almond" - elif self.entry.data["type"] != TYPE_LOCAL: - host = f"{host}/me" return { "text": "Would you like to opt-in to share your anonymized commands with Stanford to improve Almond's responses?", "url": f"{host}/conversation", From 138cee8069327b788f07c9922ffa8072b2f9db1a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Nov 2019 13:05:54 -0800 Subject: [PATCH 306/306] Version bump to 0.102.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cb3b234595c..ef16de573a0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1)