From 47220ae4a6b2a786d0d1f6e6eb5775ae45820816 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 24 Jun 2020 17:52:56 -0400 Subject: [PATCH 001/428] Handle Centralite Pearl thermostat modes (#37065) --- homeassistant/components/zha/climate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index fc57b46ec34..7e2a0e147a7 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -587,3 +587,13 @@ class ZenWithinThermostat(Thermostat): if self.hvac_mode != HVAC_MODE_OFF: return CURRENT_HVAC_IDLE return CURRENT_HVAC_OFF + + +@STRICT_MATCH( + channel_names=CHANNEL_THERMOSTAT, + aux_channels=CHANNEL_FAN, + manufacturers="Centralite", + models="3157100", +) +class CentralitePearl(ZenWithinThermostat): + """Centralite Pearl Thermostat implementation.""" From bd42827d433d502119494225aa96e644da0db15e Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 24 Jun 2020 17:54:11 -0400 Subject: [PATCH 002/428] Don't reset multiplier/divisor on failures (#37066) If SmartEnergy.Metering channels fails to get multiplier/divisor when initializing, then keep the old values instead of resetting to 1. --- .../components/zha/core/channels/smartenergy.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 2e9b69be069..7b12411b84f 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -80,8 +80,8 @@ class Metering(ZigbeeChannel): ) -> None: """Initialize Metering.""" super().__init__(cluster, ch_pool) - self._divisor = None - self._multiplier = None + self._divisor = 1 + self._multiplier = 1 self._unit_enum = None self._format_spec = None @@ -114,14 +114,8 @@ class Metering(ZigbeeChannel): from_cache=from_cache, ) - self._divisor = results.get("divisor", 1) - if self._divisor == 0: - self._divisor = 1 - - self._multiplier = results.get("multiplier", 1) - if self._multiplier == 0: - self._multiplier = 1 - + self._divisor = results.get("divisor", self._divisor) + self._multiplier = results.get("multiplier", self._multiplier) self._unit_enum = results.get("unit_of_measure", 0x7F) # default to unknown fmting = results.get( From 9f855c7d01edd75bc8138af5f4b02849b3ff67ea Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Wed, 24 Jun 2020 19:02:03 -0400 Subject: [PATCH 003/428] Zerproc cleanup (#37072) --- homeassistant/components/zerproc/light.py | 4 ++-- homeassistant/components/zerproc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 0a7141efe8a..0fa18197764 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -95,7 +95,7 @@ async def async_setup_entry( async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) -class ZerprocLight(Light): +class ZerprocLight(LightEntity): """Representation of an Zerproc Light.""" def __init__(self, light): diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json index f00a8bdc885..4f9b559bc19 100644 --- a/homeassistant/components/zerproc/manifest.json +++ b/homeassistant/components/zerproc/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zerproc", "requirements": [ - "pyzerproc==0.2.4" + "pyzerproc==0.2.5" ], "codeowners": [ "@emlove" diff --git a/requirements_all.txt b/requirements_all.txt index c00d3d3dfd3..1b21e45c71b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1836,7 +1836,7 @@ pyzabbix==0.7.4 pyzbar==0.1.7 # homeassistant.components.zerproc -pyzerproc==0.2.4 +pyzerproc==0.2.5 # homeassistant.components.qnap qnapstats==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6db4ba37354..2c8cdba6c5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -796,7 +796,7 @@ pyvizio==0.1.48 pywebpush==1.9.2 # homeassistant.components.zerproc -pyzerproc==0.2.4 +pyzerproc==0.2.5 # homeassistant.components.rachio rachiopy==0.1.3 From cbb76be9d02659afe4551845dcc1354c4b70322c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 25 Jun 2020 00:08:30 +0000 Subject: [PATCH 004/428] [ci skip] Translation update --- .../components/avri/translations/lb.json | 2 + .../components/awair/translations/lb.json | 22 ++++++++++ .../components/climate/translations/pl.json | 2 +- .../components/daikin/translations/ca.json | 2 +- .../components/directv/translations/ca.json | 2 +- .../components/dunehd/translations/ca.json | 2 +- .../components/gogogate2/translations/ca.json | 4 +- .../huawei_lte/translations/lb.json | 1 + .../humidifier/translations/ca.json | 18 +++++++++ .../humidifier/translations/es.json | 18 +++++++++ .../humidifier/translations/lb.json | 18 +++++++++ .../humidifier/translations/pl.json | 18 +++++++++ .../humidifier/translations/ru.json | 18 +++++++++ .../components/ipp/translations/ca.json | 4 +- .../components/metoffice/translations/ca.json | 2 +- .../components/mill/translations/ca.json | 2 +- .../components/mqtt/translations/lb.json | 32 +++++++++++++++ .../components/pi_hole/translations/ca.json | 2 +- .../plum_lightpad/translations/ca.json | 18 +++++++++ .../plum_lightpad/translations/en.json | 12 +++--- .../plum_lightpad/translations/es.json | 18 +++++++++ .../plum_lightpad/translations/lb.json | 18 +++++++++ .../plum_lightpad/translations/ru.json | 18 +++++++++ .../components/roku/translations/ca.json | 2 +- .../components/smappee/translations/es.json | 2 +- .../components/sms/translations/ca.json | 2 +- .../components/sms/translations/lb.json | 20 ++++++++++ .../components/soma/translations/es.json | 2 +- .../components/sonarr/translations/ca.json | 2 +- .../components/songpal/translations/ca.json | 2 +- .../components/spotify/translations/es.json | 2 +- .../squeezebox/translations/ca.json | 2 +- .../squeezebox/translations/lb.json | 33 +++++++++++++++ .../components/toon/translations/ca.json | 4 ++ .../components/toon/translations/es.json | 14 +++++++ .../components/toon/translations/lb.json | 14 +++++++ .../components/toon/translations/ru.json | 14 +++++++ .../components/tuya/translations/ca.json | 2 +- .../components/vizio/translations/ca.json | 2 +- .../components/withings/translations/es.json | 2 +- .../components/withings/translations/lb.json | 4 ++ .../xiaomi_aqara/translations/lb.json | 40 +++++++++++++++++++ 42 files changed, 389 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/awair/translations/lb.json create mode 100644 homeassistant/components/humidifier/translations/ca.json create mode 100644 homeassistant/components/humidifier/translations/es.json create mode 100644 homeassistant/components/humidifier/translations/lb.json create mode 100644 homeassistant/components/humidifier/translations/pl.json create mode 100644 homeassistant/components/humidifier/translations/ru.json create mode 100644 homeassistant/components/plum_lightpad/translations/ca.json create mode 100644 homeassistant/components/plum_lightpad/translations/es.json create mode 100644 homeassistant/components/plum_lightpad/translations/lb.json create mode 100644 homeassistant/components/plum_lightpad/translations/ru.json create mode 100644 homeassistant/components/sms/translations/lb.json create mode 100644 homeassistant/components/squeezebox/translations/lb.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/lb.json diff --git a/homeassistant/components/avri/translations/lb.json b/homeassistant/components/avri/translations/lb.json index b54cb6c2cc2..7bbafbce010 100644 --- a/homeassistant/components/avri/translations/lb.json +++ b/homeassistant/components/avri/translations/lb.json @@ -4,11 +4,13 @@ "already_configured": "D\u00ebs Adress ass scho konfigur\u00e9iert." }, "error": { + "invalid_country_code": "Onbekannte Zweestellege L\u00e4nner Code", "invalid_house_number": "Ong\u00eblteg Haus Nummer" }, "step": { "user": { "data": { + "country_code": "Zweestellege L\u00e4nner Code", "house_number": "Haus Nummer", "house_number_extension": "Haus Nummer Extensioun", "zip_code": "Postleitzuel" diff --git a/homeassistant/components/awair/translations/lb.json b/homeassistant/components/awair/translations/lb.json new file mode 100644 index 00000000000..d9ef0d9c1a7 --- /dev/null +++ b/homeassistant/components/awair/translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "unknown": "Onbekannten Awair API Feeler" + }, + "step": { + "reauth": { + "data": { + "access_token": "Acc\u00e8s Jeton", + "email": "E-Mail" + }, + "description": "G\u00ebff d\u00e4in Awair Developpeur Acc\u00e8s jeton nach emol un." + }, + "user": { + "data": { + "access_token": "Acc\u00e8s Jeton", + "email": "E-Mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/pl.json b/homeassistant/components/climate/translations/pl.json index 50f882dcd80..885938c2562 100644 --- a/homeassistant/components/climate/translations/pl.json +++ b/homeassistant/components/climate/translations/pl.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "zmie\u0144 tryb HVAC na {entity_name}", + "set_hvac_mode": "zmie\u0144 tryb pracy dla {entity_name}", "set_preset_mode": "zmie\u0144 ustawienia dla {entity_name}" }, "condition_type": { diff --git a/homeassistant/components/daikin/translations/ca.json b/homeassistant/components/daikin/translations/ca.json index 825c637610a..724cdac5f56 100644 --- a/homeassistant/components/daikin/translations/ca.json +++ b/homeassistant/components/daikin/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "device_fail": "Error inesperat", - "device_timeout": "No s'ha pogut connectar", + "device_timeout": "Ha fallat la connexi\u00f3", "forbidden": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { diff --git a/homeassistant/components/directv/translations/ca.json b/homeassistant/components/directv/translations/ca.json index be96883d2d0..57db4ee0030 100644 --- a/homeassistant/components/directv/translations/ca.json +++ b/homeassistant/components/directv/translations/ca.json @@ -5,7 +5,7 @@ "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/dunehd/translations/ca.json b/homeassistant/components/dunehd/translations/ca.json index b1679a414c0..a62d94cfff9 100644 --- a/homeassistant/components/dunehd/translations/ca.json +++ b/homeassistant/components/dunehd/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids." }, "step": { diff --git a/homeassistant/components/gogogate2/translations/ca.json b/homeassistant/components/gogogate2/translations/ca.json index 31c130e00c7..02e61d3d7a0 100644 --- a/homeassistant/components/gogogate2/translations/ca.json +++ b/homeassistant/components/gogogate2/translations/ca.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "error": { - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { diff --git a/homeassistant/components/huawei_lte/translations/lb.json b/homeassistant/components/huawei_lte/translations/lb.json index 16c8c8eaafc..8c38dffacf7 100644 --- a/homeassistant/components/huawei_lte/translations/lb.json +++ b/homeassistant/components/huawei_lte/translations/lb.json @@ -16,6 +16,7 @@ "response_error": "Onbekannte Feeler vum Apparat", "unknown_connection_error": "Onbekannte Feeler beim verbannen mam Apparat" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/humidifier/translations/ca.json b/homeassistant/components/humidifier/translations/ca.json new file mode 100644 index 00000000000..353e590d59b --- /dev/null +++ b/homeassistant/components/humidifier/translations/ca.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Estableix la humitat de {entity_name}", + "set_mode": "Canvia el mode de {entity_name}", + "toggle": "Commuta {entity_name}", + "turn_off": "Apaga {entity_name}", + "turn_on": "Enc\u00e9n {entity_name}" + } + }, + "state": { + "_": { + "off": "OFF", + "on": "ON" + } + }, + "title": "Humidificador" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json new file mode 100644 index 00000000000..2c867e0bc73 --- /dev/null +++ b/homeassistant/components/humidifier/translations/es.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Establecer humedad para {entity_name}", + "set_mode": "Cambiar modo en {entity_name}", + "toggle": "Alternar {entity_name}", + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + } + }, + "state": { + "_": { + "off": "Apagado", + "on": "Encendido" + } + }, + "title": "Humidificador" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/lb.json b/homeassistant/components/humidifier/translations/lb.json new file mode 100644 index 00000000000..3dc1261132f --- /dev/null +++ b/homeassistant/components/humidifier/translations/lb.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Loftfiichtegkeet setze fir {entity_name}", + "set_mode": "Modus \u00e4nnere fir {entity_name}", + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + } + }, + "state": { + "_": { + "off": "Aus", + "on": "Un" + } + }, + "title": "Loftbefiichter" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/pl.json b/homeassistant/components/humidifier/translations/pl.json new file mode 100644 index 00000000000..0a57eede3b3 --- /dev/null +++ b/homeassistant/components/humidifier/translations/pl.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "ustaw wilgotno\u015b\u0107 dla {entity_name}", + "set_mode": "zmie\u0144 tryb pracy dla {entity_name}", + "toggle": "prze\u0142\u0105cz {entity_name}", + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" + } + }, + "state": { + "_": { + "off": "wy\u0142\u0105czony", + "on": "w\u0142\u0105czony" + } + }, + "title": "Nawil\u017cacz" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/ru.json b/homeassistant/components/humidifier/translations/ru.json new file mode 100644 index 00000000000..32e19e8325b --- /dev/null +++ b/homeassistant/components/humidifier/translations/ru.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "\u0417\u0430\u0434\u0430\u0442\u044c \u0446\u0435\u043b\u0435\u0432\u0443\u044e \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u044c {entity_name}", + "set_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}\"", + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "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}" + } + }, + "state": { + "_": { + "off": "\u0412\u044b\u043a\u043b", + "on": "\u0412\u043a\u043b" + } + }, + "title": "\u0423\u0432\u043b\u0430\u0436\u043d\u0438\u0442\u0435\u043b\u044c" +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ca.json b/homeassistant/components/ipp/translations/ca.json index cd619583e52..6e3185767d5 100644 --- a/homeassistant/components/ipp/translations/ca.json +++ b/homeassistant/components/ipp/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "connection_error": "No s'ha pogut connectar", + "connection_error": "Ha fallat la connexi\u00f3", "connection_upgrade": "No s'ha pogut connectar amb la impressora, es necessita actualitzar la connexi\u00f3.", "ipp_error": "S'ha produ\u00eft un error IPP.", "ipp_version_error": "La versi\u00f3 IPP no \u00e9s compatible amb la impressora.", @@ -10,7 +10,7 @@ "unique_id_required": "Falta la identificaci\u00f3 \u00fanica al dispositiu, necess\u00e0ria per al descobriment." }, "error": { - "connection_error": "No s'ha pogut connectar", + "connection_error": "Ha fallat la connexi\u00f3", "connection_upgrade": "No s'ha pogut connectar amb la impressora. Prova-ho novament amb l'opci\u00f3 SSL/TLS activada." }, "flow_title": "Impressora: {name}", diff --git a/homeassistant/components/metoffice/translations/ca.json b/homeassistant/components/metoffice/translations/ca.json index 6b90228c254..b37b2994a6b 100644 --- a/homeassistant/components/metoffice/translations/ca.json +++ b/homeassistant/components/metoffice/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/mill/translations/ca.json b/homeassistant/components/mill/translations/ca.json index 0e877ca33cc..e7f97e600d0 100644 --- a/homeassistant/components/mill/translations/ca.json +++ b/homeassistant/components/mill/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El compte ja ha estat configurat" }, "error": { - "connection_error": "No s'ha pogut connectar" + "connection_error": "Ha fallat la connexi\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/mqtt/translations/lb.json b/homeassistant/components/mqtt/translations/lb.json index 4ffa66b10bd..1ee1b8b1c1e 100644 --- a/homeassistant/components/mqtt/translations/lb.json +++ b/homeassistant/components/mqtt/translations/lb.json @@ -47,5 +47,37 @@ "button_short_release": "\"{subtype}\" lassgelooss", "button_triple_press": "\"{subtype}\" dr\u00e4imol gedr\u00e9ckt" } + }, + "options": { + "error": { + "bad_birth": "Ong\u00ebltege birth topic.", + "bad_will": "Ong\u00ebltege will topic.", + "cannot_connect": "Kann sech net mam Broker verbannen." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "description": "G\u00ebff Verbindungs Informatioune vun dengem MQTT Broker an." + }, + "options": { + "data": { + "birth_payload": "Birth message payload", + "birth_qos": "Birth message QoS", + "birth_retain": "Birth message retain", + "birth_topic": "Birth message topic", + "discovery": "Entdeckung aktiv\u00e9ieren", + "will_payload": "Will message payload", + "will_qos": "Will message QoS", + "will_retain": "Will message retain", + "will_topic": "Will message topic" + }, + "description": "Wiel MQTT Optiounen aus." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index 7ba84de21f7..f9f3f1d37d7 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -5,7 +5,7 @@ "duplicated_name": "El nom ja existeix" }, "error": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/plum_lightpad/translations/ca.json b/homeassistant/components/plum_lightpad/translations/ca.json new file mode 100644 index 00000000000..86f649d57d7 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/en.json b/homeassistant/components/plum_lightpad/translations/en.json index 95cafaa7313..ebf44baf217 100644 --- a/homeassistant/components/plum_lightpad/translations/en.json +++ b/homeassistant/components/plum_lightpad/translations/en.json @@ -1,20 +1,18 @@ { "config": { "abort": { - "single_instance_per_username_allowed": "Only one config entry per unique username is supported" + "already_configured": "Account is already configured" }, "error": { - "cannot_connect": "Unable to connect to Plum Cloud." + "cannot_connect": "Failed to connect" }, "step": { "user": { "data": { "password": "Password", "username": "Email" - }, - "title": "Fill in your Plum Cloud login information" + } } } - }, - "title": "Plum Lightpad" -} + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/es.json b/homeassistant/components/plum_lightpad/translations/es.json new file mode 100644 index 00000000000..b5f3c8b1439 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/lb.json b/homeassistant/components/plum_lightpad/translations/lb.json new file mode 100644 index 00000000000..226a441d514 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/ru.json b/homeassistant/components/plum_lightpad/translations/ru.json new file mode 100644 index 00000000000..12f2e4a01c5 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index 7044b9c7bc0..e9ab61575b5 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -5,7 +5,7 @@ "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index 57595e99da8..f9b65b5339a 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n.", "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." }, diff --git a/homeassistant/components/sms/translations/ca.json b/homeassistant/components/sms/translations/ca.json index 2d2a609c43e..f7640befed4 100644 --- a/homeassistant/components/sms/translations/ca.json +++ b/homeassistant/components/sms/translations/ca.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/sms/translations/lb.json b/homeassistant/components/sms/translations/lb.json new file mode 100644 index 00000000000..fd68a7a7170 --- /dev/null +++ b/homeassistant/components/sms/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng Konfiguratioun ass m\u00e9iglech." + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "device": "Apparat" + }, + "title": "Mam Modem verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/es.json b/homeassistant/components/soma/translations/es.json index 008404df599..6f6b47275dd 100644 --- a/homeassistant/components/soma/translations/es.json +++ b/homeassistant/components/soma/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", - "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "connection_error": "No se ha podido conectar a SOMA Connect.", "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", "result_error": "SOMA Connect respondi\u00f3 con un error." diff --git a/homeassistant/components/sonarr/translations/ca.json b/homeassistant/components/sonarr/translations/ca.json index 955d94e9202..ed59caf89df 100644 --- a/homeassistant/components/sonarr/translations/ca.json +++ b/homeassistant/components/sonarr/translations/ca.json @@ -5,7 +5,7 @@ "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "flow_title": "Sonarr: {name}", diff --git a/homeassistant/components/songpal/translations/ca.json b/homeassistant/components/songpal/translations/ca.json index 10060f63ed4..02f3f4a4d05 100644 --- a/homeassistant/components/songpal/translations/ca.json +++ b/homeassistant/components/songpal/translations/ca.json @@ -5,7 +5,7 @@ "not_songpal_device": "No \u00e9s un dispositiu Songpal" }, "error": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "flow_title": "Sony Songpal {name} ({host})", "step": { diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index 78b71486b71..71020022ef1 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "S\u00f3lo puedes configurar una cuenta de Spotify.", - "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/squeezebox/translations/ca.json b/homeassistant/components/squeezebox/translations/ca.json index a5ee040b705..6033d7ae449 100644 --- a/homeassistant/components/squeezebox/translations/ca.json +++ b/homeassistant/components/squeezebox/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/squeezebox/translations/lb.json b/homeassistant/components/squeezebox/translations/lb.json new file mode 100644 index 00000000000..26b41e68e60 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "no_server_found": "Kee LMS Server fonnt." + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "no_server_found": "Konnt kee Server automatesch entdecken.", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Logitech Squeezebox: {host}", + "step": { + "edit": { + "data": { + "host": "Host", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "title": "Verbindungs Informatiounen \u00e4nneren" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Logitech Media Server ariichten" + } + } + }, + "title": "Logitech Squeezebox" +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json index 1ff63331f52..52e4bbe4861 100644 --- a/homeassistant/components/toon/translations/ca.json +++ b/homeassistant/components/toon/translations/ca.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "L\u2019acord seleccionat ja est\u00e0 configurat.", + "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "client_id": "L'identificador de client de la configuraci\u00f3 no \u00e9s v\u00e0lid.", "client_secret": "El codi secret de client de la configuraci\u00f3 no \u00e9s v\u00e0lid.", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_agreements": "Aquest compte no t\u00e9 pantalles Toon.", "no_app": "Has de configurar Toon abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "S'ha produ\u00eft un error inesperat durant l'autenticaci\u00f3." diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index 737cc1d0612..4861abd8b92 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "El acuerdo seleccionado ya est\u00e1 configurado.", + "authorize_url_fail": "Error desconocido generando una url de autorizaci\u00f3n", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "client_id": "El ID de cliente en la configuraci\u00f3n no es v\u00e1lido.", "client_secret": "El secreto de la configuraci\u00f3n no es v\u00e1lido.", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", "no_app": "Es necesario configurar Toon antes de poder autenticarse con \u00e9l. [Por favor, lee las instrucciones](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Se ha producido un error inesperado al autenticar." @@ -12,6 +16,13 @@ "display_exists": "La pantalla seleccionada ya est\u00e1 configurada." }, "step": { + "agreement": { + "data": { + "agreement": "Acuerdo" + }, + "description": "Selecciona la direcci\u00f3n del acuerdo que deseas a\u00f1adir.", + "title": "Selecciona tu acuerdo" + }, "authenticate": { "data": { "password": "Contrase\u00f1a", @@ -27,6 +38,9 @@ }, "description": "Selecciona la pantalla Toon que quieres conectar.", "title": "Seleccionar pantalla" + }, + "pick_implementation": { + "title": "Elige el arrendatario con el cual deseas autenticarte" } } } diff --git a/homeassistant/components/toon/translations/lb.json b/homeassistant/components/toon/translations/lb.json index 57837479d4c..9c0d3711574 100644 --- a/homeassistant/components/toon/translations/lb.json +++ b/homeassistant/components/toon/translations/lb.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "Den ausgewielten Accord ass scho konfigur\u00e9iert.", + "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", + "authorize_url_timeout": "Z\u00e4itiwwerschraidung beim erstellen vun der Autorisatioun's URL.", "client_id": "Client ID vun der Konfiguratioun ass ong\u00eblteg.", "client_secret": "Client Passwuert vun der Konfiguratioun ass ong\u00eblteg.", + "missing_configuration": "Komponent ass net konfigur\u00e9iert. Folleg der Dokumentatioun.", "no_agreements": "D\u00ebse Kont huet keen Toon Ecran.", "no_app": "Dir musst Toon konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Onerwaarte Feeler bei der Authentifikatioun." @@ -12,6 +16,13 @@ "display_exists": "Den ausgewielten Ecran ass scho konfigur\u00e9iert." }, "step": { + "agreement": { + "data": { + "agreement": "Accord" + }, + "description": "Wiel d'Adress vum Accord aus dee dob\u00e4igesaat soll ginn.", + "title": "D\u00e4in Accord auswielen" + }, "authenticate": { "data": { "password": "Passwuert", @@ -27,6 +38,9 @@ }, "description": "Wielt den Toon Ecran aus fir sech domat ze verbannen.", "title": "Ecran auswielen" + }, + "pick_implementation": { + "title": "Wielt de Locataire aus mat deem sech authentifiz\u00e9iert g\u00ebtt" } } } diff --git a/homeassistant/components/toon/translations/ru.json b/homeassistant/components/toon/translations/ru.json index afa73467479..7bdaf0eb08e 100644 --- a/homeassistant/components/toon/translations/ru.json +++ b/homeassistant/components/toon/translations/ru.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0433\u043b\u0430\u0448\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e.", + "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", "no_app": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." @@ -12,6 +16,13 @@ "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { + "agreement": { + "data": { + "agreement": "\u0421\u043e\u0433\u043b\u0430\u0448\u0435\u043d\u0438\u0435" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0441\u043e\u0433\u043b\u0430\u0448\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c.", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0412\u0430\u0448\u0435 \u0441\u043e\u0433\u043b\u0430\u0448\u0435\u043d\u0438\u0435" + }, "authenticate": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", @@ -27,6 +38,9 @@ }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 Toon \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "title": "Toon" + }, + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0440\u0435\u043d\u0434\u0430\u0442\u043e\u0440\u0430 \u0434\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } } diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index dc07a2d8715..20709b61a98 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "auth_failed": "Autenticaci\u00f3 inv\u00e0lida", - "conn_error": "No s'ha pogut connectar", + "conn_error": "Ha fallat la connexi\u00f3", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { diff --git a/homeassistant/components/vizio/translations/ca.json b/homeassistant/components/vizio/translations/ca.json index 42d363c1d41..50a9751a45d 100644 --- a/homeassistant/components/vizio/translations/ca.json +++ b/homeassistant/components/vizio/translations/ca.json @@ -5,7 +5,7 @@ "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom, les aplicacions i/o les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, l'entrada de configuraci\u00f3 s'ha actualitzat." }, "error": { - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "complete_pairing_failed": "No s'ha pogut completar l'emparellament. Verifica que el PIN proporcionat sigui el correcte i que el televisor segueix connectat a la xarxa abans de provar-ho de nou.", "host_exists": "Dispositiu VIZIO SmartCast amb aquest nom d'amfitri\u00f3 ja configurat.", "name_exists": "Dispositiu VIZIO SmartCast amb aquest nom ja configurat." diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index 1285a24b41f..392f300260a 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Configuraci\u00f3n actualizada para el perfil.", - "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/withings/translations/lb.json b/homeassistant/components/withings/translations/lb.json index c8a2d4beae2..a5102baf917 100644 --- a/homeassistant/components/withings/translations/lb.json +++ b/homeassistant/components/withings/translations/lb.json @@ -1,12 +1,16 @@ { "config": { "abort": { + "already_configured": "Konfiguratioun aktualis\u00e9iert fir de Profil.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "missing_configuration": "Withings Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." }, "create_entry": { "default": "Erfollegr\u00e4ich mat Withings authentifiz\u00e9iert." }, + "error": { + "profile_exists": "Benotzer Profil ass scho konfigur\u00e9iert. G\u00ebff eem eenzegartege Profil Numm un" + }, "flow_title": "Withing: {profile}", "step": { "pick_implementation": { diff --git a/homeassistant/components/xiaomi_aqara/translations/lb.json b/homeassistant/components/xiaomi_aqara/translations/lb.json new file mode 100644 index 00000000000..bfea843c56e --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/lb.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf fir d\u00ebs Gateway ass schonn am gaangen.", + "not_xiaomi_aqara": "Keng Xiaomi Aqara Gateway, entdeckten Apparat passt net zu bekannte Gateways" + }, + "error": { + "discovery_error": "Feeler beim entdecken vun enger Xiaomi Aqara Gateway, prob\u00e9ier d'IP vum Apparat op deem Home Assistant leeft als Interface ze benotzen", + "invalid_interface": "Ong\u00ebltege Netzwierk Interface", + "invalid_key": "Ong\u00ebltegen Gateway Schl\u00ebssel", + "not_found_error": "D\u00e9i Gateway d\u00e9i duerch de Zeroconf entdeckt gouf konnt net lokalis\u00e9iert ginn, prob\u00e9ier d'IP vum Apparat op deem Home Assistant leeft als Interface ze benotzen" + }, + "flow_title": "Xiaomi Aqara Gateway: {name}", + "step": { + "select": { + "data": { + "select_ip": "IP vun der Gateway" + }, + "description": "Setup nach emol ausf\u00e9ieren fir eng zous\u00e4tzlech Gateway dob\u00e4i ze setzen", + "title": "Xiaomi Aqara Gateway auswielen mat der sech soll verbonne ginn" + }, + "settings": { + "data": { + "key": "Schl\u00ebssel vun denger Gateway", + "name": "Numm vum Gateway" + }, + "description": "De Schl\u00ebssel (Passwuert) kann iwwert de folgenden Tutorial ausgelies ginn: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Falls kee Schl\u00ebssel uginn ass sinn n\u00ebmmen Sensoren accessibel.", + "title": "Xiaomi Aqara Gateway, optionell Astellungen" + }, + "user": { + "data": { + "interface": "Netzwierk Interface dee soll benotzt ginn" + }, + "description": "Mat denger Xiaomi Aqara Gateway verbannen", + "title": "Xiaomi Aqara Gateway" + } + } + } +} \ No newline at end of file From 7968cd650a568d54f7f5e0c3aa8933f89d7d637f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 24 Jun 2020 18:37:01 -0600 Subject: [PATCH 005/428] Add concept of allowed external URLs to config (#36988) Co-authored-by: Paulus Schoutsen --- homeassistant/config.py | 10 +++++++ homeassistant/const.py | 15 +++++----- homeassistant/core.py | 14 ++++++++++ tests/components/api/test_init.py | 2 ++ .../components/websocket_api/test_commands.py | 4 +++ tests/test_core.py | 28 +++++++++++++++++++ 6 files changed, 66 insertions(+), 7 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2d90838cb9d..894d0bb5379 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, + CONF_ALLOWLIST_EXTERNAL_URLS, CONF_AUTH_MFA_MODULES, CONF_AUTH_PROVIDERS, CONF_CUSTOMIZE, @@ -185,6 +186,7 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( vol.Optional(CONF_WHITELIST_EXTERNAL_DIRS): vol.All( cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter ), + vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(cv.ensure_list, [cv.url]), vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, vol.Optional(CONF_AUTH_PROVIDERS): vol.All( cv.ensure_list, @@ -502,6 +504,14 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non if CONF_WHITELIST_EXTERNAL_DIRS in config: hac.whitelist_external_dirs.update(set(config[CONF_WHITELIST_EXTERNAL_DIRS])) + # Init whitelist external URL list – make sure to add / to every URL that doesn't + # already have it so that we can properly test "path ownership" + if CONF_ALLOWLIST_EXTERNAL_URLS in config: + hac.allowlist_external_urls.update( + url if url.endswith("/") else f"{url}/" + for url in config[CONF_ALLOWLIST_EXTERNAL_URLS] + ) + # Customize cust_exact = dict(config[CONF_CUSTOMIZE]) cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN]) diff --git a/homeassistant/const.py b/homeassistant/const.py index c95c6ec48cc..900df1f9336 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -32,13 +32,14 @@ CONF_ACCESS_TOKEN = "access_token" CONF_ADDRESS = "address" CONF_AFTER = "after" CONF_ALIAS = "alias" +CONF_ALLOWLIST_EXTERNAL_URLS = "allowlist_external_urls" CONF_API_KEY = "api_key" CONF_API_VERSION = "api_version" CONF_ARMING_TIME = "arming_time" CONF_AT = "at" +CONF_AUTHENTICATION = "authentication" CONF_AUTH_MFA_MODULES = "auth_mfa_modules" CONF_AUTH_PROVIDERS = "auth_providers" -CONF_AUTHENTICATION = "authentication" CONF_BASE = "base" CONF_BEFORE = "before" CONF_BELOW = "below" @@ -68,9 +69,9 @@ CONF_CUSTOMIZE_GLOB = "customize_glob" CONF_DELAY = "delay" CONF_DELAY_TIME = "delay_time" CONF_DEVICE = "device" +CONF_DEVICES = "devices" CONF_DEVICE_CLASS = "device_class" CONF_DEVICE_ID = "device_id" -CONF_DEVICES = "devices" CONF_DISARM_AFTER_TRIGGER = "disarm_after_trigger" CONF_DISCOVERY = "discovery" CONF_DISKS = "disks" @@ -90,8 +91,8 @@ CONF_EVENT_DATA = "event_data" CONF_EVENT_DATA_TEMPLATE = "event_data_template" CONF_EXCLUDE = "exclude" CONF_EXTERNAL_URL = "external_url" -CONF_FILE_PATH = "file_path" CONF_FILENAME = "filename" +CONF_FILE_PATH = "file_path" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" CONF_FRIENDLY_NAME = "friendly_name" @@ -138,15 +139,15 @@ CONF_RADIUS = "radius" CONF_RECIPIENT = "recipient" CONF_REGION = "region" CONF_RESOURCE = "resource" -CONF_RESOURCE_TEMPLATE = "resource_template" CONF_RESOURCES = "resources" +CONF_RESOURCE_TEMPLATE = "resource_template" CONF_RGB = "rgb" CONF_ROOM = "room" CONF_SCAN_INTERVAL = "scan_interval" CONF_SCENE = "scene" CONF_SENDER = "sender" -CONF_SENSOR_TYPE = "sensor_type" CONF_SENSORS = "sensors" +CONF_SENSOR_TYPE = "sensor_type" CONF_SERVICE = "service" CONF_SERVICE_DATA = "data" CONF_SERVICE_TEMPLATE = "service_template" @@ -159,8 +160,8 @@ CONF_STATE_TEMPLATE = "state_template" CONF_STRUCTURE = "structure" CONF_SWITCHES = "switches" CONF_TEMPERATURE_UNIT = "temperature_unit" -CONF_TIME_ZONE = "time_zone" CONF_TIMEOUT = "timeout" +CONF_TIME_ZONE = "time_zone" CONF_TOKEN = "token" CONF_TRIGGER_TIME = "trigger_time" CONF_TTL = "ttl" @@ -174,9 +175,9 @@ CONF_VERIFY_SSL = "verify_ssl" CONF_WAIT_TEMPLATE = "wait_template" CONF_WEBHOOK_ID = "webhook_id" CONF_WEEKDAY = "weekday" -CONF_WHITE_VALUE = "white_value" CONF_WHITELIST = "whitelist" CONF_WHITELIST_EXTERNAL_DIRS = "whitelist_external_dirs" +CONF_WHITE_VALUE = "white_value" CONF_XY = "xy" CONF_ZONE = "zone" diff --git a/homeassistant/core.py b/homeassistant/core.py index f8f4e7c0d02..da4c5b56146 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1332,6 +1332,9 @@ class Config: # List of allowed external dirs to access self.whitelist_external_dirs: Set[str] = set() + # List of allowed external URLs that integrations may use + self.allowlist_external_urls: Set[str] = set() + # If Home Assistant is running in safe mode self.safe_mode: bool = False @@ -1353,6 +1356,16 @@ class Config: raise HomeAssistantError("config_dir is not set") return os.path.join(self.config_dir, *path) + def is_allowed_external_url(self, url: str) -> bool: + """Check if an external URL is allowed.""" + parsed_url = f"{str(yarl.URL(url))}/" + + return any( + allowed + for allowed in self.allowlist_external_urls + if parsed_url.startswith(allowed) + ) + def is_allowed_path(self, path: str) -> bool: """Check if the path is valid for access from outside.""" assert path is not None @@ -1395,6 +1408,7 @@ class Config: "components": self.components, "config_dir": self.config_dir, "whitelist_external_dirs": self.whitelist_external_dirs, + "allowlist_external_urls": self.allowlist_external_urls, "version": __version__, "config_source": self.config_source, "safe_mode": self.safe_mode, diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 1c93158ec03..24a1532dead 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -212,6 +212,8 @@ async def test_api_get_config(hass, mock_api_client): result["components"] = set(result["components"]) if "whitelist_external_dirs" in result: result["whitelist_external_dirs"] = set(result["whitelist_external_dirs"]) + if "allowlist_external_urls" in result: + result["allowlist_external_urls"] = set(result["allowlist_external_urls"]) assert hass.config.as_dict() == result diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 3754767dd9e..6f7b288f292 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -234,6 +234,10 @@ async def test_get_config(hass, websocket_client): msg["result"]["whitelist_external_dirs"] = set( msg["result"]["whitelist_external_dirs"] ) + if "allowlist_external_urls" in msg["result"]: + msg["result"]["allowlist_external_urls"] = set( + msg["result"]["allowlist_external_urls"] + ) assert msg["result"] == hass.config.as_dict() diff --git a/tests/test_core.py b/tests/test_core.py index c4079328f1f..32634313a48 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -915,6 +915,7 @@ class TestConfig(unittest.TestCase): "components": set(), "config_dir": "/test/ha-config", "whitelist_external_dirs": set(), + "allowlist_external_urls": set(), "version": __version__, "config_source": "default", "safe_mode": False, @@ -955,6 +956,33 @@ class TestConfig(unittest.TestCase): with pytest.raises(AssertionError): self.config.is_allowed_path(None) + def test_is_allowed_external_url(self): + """Test is_allowed_external_url method.""" + self.config.allowlist_external_urls = [ + "http://x.com/", + "https://y.com/bla/", + "https://z.com/images/1.jpg/", + ] + + valid = [ + "http://x.com/1.jpg", + "http://x.com", + "https://y.com/bla/", + "https://y.com/bla/2.png", + "https://z.com/images/1.jpg", + ] + for url in valid: + assert self.config.is_allowed_external_url(url) + + invalid = [ + "https://a.co", + "https://y.com/bla_wrong", + "https://y.com/bla/../image.jpg", + "https://z.com/images", + ] + for url in invalid: + assert not self.config.is_allowed_external_url(url) + async def test_event_on_update(hass): """Test that event is fired on update.""" From 5bc6ed4cefe884f7f914b9ce0e93f266fae021ba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Jun 2020 18:14:50 -0700 Subject: [PATCH 006/428] Add logbook platforms (#37078) * Add logbook platforms * Fix logbook describe test --- homeassistant/components/alexa/__init__.py | 24 -------- homeassistant/components/alexa/logbook.py | 28 +++++++++ homeassistant/components/alexa/manifest.json | 13 +++- .../components/automation/__init__.py | 13 ---- .../components/automation/logbook.py | 23 ++++++++ .../components/automation/manifest.json | 9 ++- homeassistant/components/homekit/__init__.py | 24 -------- homeassistant/components/homekit/logbook.py | 28 +++++++++ .../components/homekit/manifest.json | 22 +++++-- homeassistant/components/logbook/__init__.py | 25 +++++--- homeassistant/components/script/__init__.py | 13 ---- homeassistant/components/script/logbook.py | 21 +++++++ homeassistant/components/script/manifest.json | 5 +- homeassistant/helpers/integration_platform.py | 16 ++++- tests/components/alexa/test_init.py | 2 + tests/components/automation/test_init.py | 2 + tests/components/homekit/test_init.py | 2 + tests/components/logbook/test_init.py | 59 ++++++++++++------- tests/components/script/test_init.py | 2 + 19 files changed, 215 insertions(+), 116 deletions(-) create mode 100644 homeassistant/components/alexa/logbook.py create mode 100644 homeassistant/components/automation/logbook.py create mode 100644 homeassistant/components/homekit/logbook.py create mode 100644 homeassistant/components/script/logbook.py diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index e8efa8a4752..7522b7e2d58 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -4,7 +4,6 @@ import logging import voluptuous as vol from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME -from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entityfilter from . import flash_briefings, intent, smart_home_http @@ -23,7 +22,6 @@ from .const import ( CONF_TITLE, CONF_UID, DOMAIN, - EVENT_ALEXA_SMART_HOME, ) _LOGGER = logging.getLogger(__name__) @@ -82,28 +80,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Activate the Alexa component.""" - - @callback - def async_describe_logbook_event(event): - """Describe a logbook event.""" - data = event.data - entity_id = data["request"].get("entity_id") - - if entity_id: - state = hass.states.get(entity_id) - name = state.name if state else entity_id - message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}" - else: - message = ( - f"send command {data['request']['namespace']}/{data['request']['name']}" - ) - - return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id} - - hass.components.logbook.async_describe_event( - DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event - ) - if DOMAIN not in config: return True diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py new file mode 100644 index 00000000000..efc188a7f8b --- /dev/null +++ b/homeassistant/components/alexa/logbook.py @@ -0,0 +1,28 @@ +"""Describe logbook events.""" +from homeassistant.core import callback + +from .const import DOMAIN, EVENT_ALEXA_SMART_HOME + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + data = event.data + entity_id = data["request"].get("entity_id") + + if entity_id: + state = hass.states.get(entity_id) + name = state.name if state else entity_id + message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}" + else: + message = ( + f"send command {data['request']['namespace']}/{data['request']['name']}" + ) + + return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id} + + async_describe_event(DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event) diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 6144ccc6870..1ed91866cdc 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -2,7 +2,14 @@ "domain": "alexa", "name": "Amazon Alexa", "documentation": "https://www.home-assistant.io/integrations/alexa", - "dependencies": ["http"], - "after_dependencies": ["logbook", "camera"], - "codeowners": ["@home-assistant/cloud", "@ochlocracy"] + "dependencies": [ + "http" + ], + "after_dependencies": [ + "camera" + ], + "codeowners": [ + "@home-assistant/cloud", + "@ochlocracy" + ] } diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 8b2c036034b..e5f2f611cdb 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -222,19 +222,6 @@ async def async_setup(hass, config): hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) ) - @callback - def async_describe_logbook_event(event): - """Describe a logbook event.""" - return { - "name": event.data.get(ATTR_NAME), - "message": "has been triggered", - "entity_id": event.data.get(ATTR_ENTITY_ID), - } - - hass.components.logbook.async_describe_event( - DOMAIN, EVENT_AUTOMATION_TRIGGERED, async_describe_logbook_event - ) - return True diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py new file mode 100644 index 00000000000..2e3ad2475fc --- /dev/null +++ b/homeassistant/components/automation/logbook.py @@ -0,0 +1,23 @@ +"""Describe logbook events.""" +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME +from homeassistant.core import callback + +from . import DOMAIN, EVENT_AUTOMATION_TRIGGERED + + +@callback +def async_describe_events(hass, async_describe_event): # type: ignore + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): # type: ignore + """Describe a logbook event.""" + return { + "name": event.data.get(ATTR_NAME), + "message": "has been triggered", + "entity_id": event.data.get(ATTR_ENTITY_ID), + } + + async_describe_event( + DOMAIN, EVENT_AUTOMATION_TRIGGERED, async_describe_logbook_event + ) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index a93baa0528a..a8dc43844e0 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -2,7 +2,12 @@ "domain": "automation", "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", - "after_dependencies": ["device_automation", "logbook", "webhook"], - "codeowners": ["@home-assistant/core"], + "after_dependencies": [ + "device_automation", + "webhook" + ], + "codeowners": [ + "@home-assistant/core" + ], "quality_scale": "internal" } diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 276a23b7354..c5a921a9dd2 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -21,7 +21,6 @@ from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, - ATTR_SERVICE, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, @@ -41,12 +40,10 @@ from .accessories import get_accessory from .aidmanager import AccessoryAidStorage from .const import ( AID_STORAGE, - ATTR_DISPLAY_NAME, ATTR_INTERGRATION, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SOFTWARE_VERSION, - ATTR_VALUE, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CONF_ADVERTISE_IP, @@ -64,7 +61,6 @@ from .const import ( DEFAULT_PORT, DEFAULT_SAFE_MODE, DOMAIN, - EVENT_HOMEKIT_CHANGED, HOMEKIT, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, @@ -325,26 +321,6 @@ def _async_register_events_and_services(hass: HomeAssistant): schema=RESET_ACCESSORY_SERVICE_SCHEMA, ) - @callback - def async_describe_logbook_event(event): - """Describe a logbook event.""" - data = event.data - entity_id = data.get(ATTR_ENTITY_ID) - value = data.get(ATTR_VALUE) - - value_msg = f" to {value}" if value else "" - message = f"send command {data[ATTR_SERVICE]}{value_msg} for {data[ATTR_DISPLAY_NAME]}" - - return { - "name": "HomeKit", - "message": message, - "entity_id": entity_id, - } - - hass.components.logbook.async_describe_event( - DOMAIN, EVENT_HOMEKIT_CHANGED, async_describe_logbook_event - ) - async def async_handle_homekit_service_start(service): """Handle start HomeKit service call.""" for entry_id in hass.data[DOMAIN]: diff --git a/homeassistant/components/homekit/logbook.py b/homeassistant/components/homekit/logbook.py new file mode 100644 index 00000000000..0ea5a5d542a --- /dev/null +++ b/homeassistant/components/homekit/logbook.py @@ -0,0 +1,28 @@ +"""Describe logbook events.""" +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE +from homeassistant.core import callback + +from .const import ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN, EVENT_HOMEKIT_CHANGED + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + data = event.data + entity_id = data.get(ATTR_ENTITY_ID) + value = data.get(ATTR_VALUE) + + value_msg = f" to {value}" if value else "" + message = f"send command {data[ATTR_SERVICE]}{value_msg} for {data[ATTR_DISPLAY_NAME]}" + + return { + "name": "HomeKit", + "message": message, + "entity_id": entity_id, + } + + async_describe_event(DOMAIN, EVENT_HOMEKIT_CHANGED, async_describe_logbook_event) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 985fcc1e799..8a5fc90ae07 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,9 +2,23 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.9.1","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1","PyTurboJPEG==1.4.0"], - "dependencies": ["http", "camera", "ffmpeg"], - "after_dependencies": ["logbook", "zeroconf"], - "codeowners": ["@bdraco"], + "requirements": [ + "HAP-python==2.9.1", + "fnvhash==0.1.0", + "PyQRCode==1.2.1", + "base36==0.1.1", + "PyTurboJPEG==1.4.0" + ], + "dependencies": [ + "http", + "camera", + "ffmpeg" + ], + "after_dependencies": [ + "zeroconf" + ], + "codeowners": [ + "@bdraco" + ], "config_flow": true } diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 13253300cf3..28d6c7fcd48 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -47,6 +47,9 @@ from homeassistant.helpers.entityfilter import ( convert_include_exclude_filter, generate_filter, ) +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util @@ -102,15 +105,9 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) -@bind_hass -def async_describe_event(hass, domain, event_name, describe_callback): - """Teach logbook how to describe a new event.""" - hass.data.setdefault(DOMAIN, {})[event_name] = (domain, describe_callback) - - async def async_setup(hass, config): """Logbook setup.""" - hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN] = {} @callback def log_message(service): @@ -131,9 +128,23 @@ async def async_setup(hass, config): ) hass.services.async_register(DOMAIN, "log", log_message, schema=LOG_MESSAGE_SCHEMA) + + await async_process_integration_platforms(hass, DOMAIN, _process_logbook_platform) + return True +async def _process_logbook_platform(hass, domain, platform): + """Process a logbook platform.""" + + @callback + def _async_describe_event(domain, event_name, describe_callback): + """Teach logbook how to describe a new event.""" + hass.data[DOMAIN][event_name] = (domain, describe_callback) + + platform.async_describe_events(hass, _async_describe_event) + + class LogbookView(HomeAssistantView): """Handle logbook view requests.""" diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index b9043ff2f09..e80dcfa8027 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -188,19 +188,6 @@ async def async_setup(hass, config): DOMAIN, SERVICE_TOGGLE, toggle_service, schema=SCRIPT_TURN_ONOFF_SCHEMA ) - @callback - def async_describe_logbook_event(event): - """Describe the logbook event.""" - return { - "name": event.data.get(ATTR_NAME), - "message": "started", - "entity_id": event.data.get(ATTR_ENTITY_ID), - } - - hass.components.logbook.async_describe_event( - DOMAIN, EVENT_SCRIPT_STARTED, async_describe_logbook_event - ) - return True diff --git a/homeassistant/components/script/logbook.py b/homeassistant/components/script/logbook.py new file mode 100644 index 00000000000..72ff0d15fc7 --- /dev/null +++ b/homeassistant/components/script/logbook.py @@ -0,0 +1,21 @@ +"""Describe logbook events.""" +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME +from homeassistant.core import callback + +from . import DOMAIN, EVENT_SCRIPT_STARTED + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): + """Describe the logbook event.""" + return { + "name": event.data.get(ATTR_NAME), + "message": "started", + "entity_id": event.data.get(ATTR_ENTITY_ID), + } + + async_describe_event(DOMAIN, EVENT_SCRIPT_STARTED, async_describe_logbook_event) diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index 9348469d258..b9d333ce553 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -2,7 +2,8 @@ "domain": "script", "name": "Scripts", "documentation": "https://www.home-assistant.io/integrations/script", - "after_dependencies": ["logbook"], - "codeowners": ["@home-assistant/core"], + "codeowners": [ + "@home-assistant/core" + ], "quality_scale": "internal" } diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index 01567c72c7b..93f3f3f7427 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -4,7 +4,7 @@ import logging from typing import Any, Awaitable, Callable from homeassistant.core import Event, HomeAssistant -from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass +from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED _LOGGER = logging.getLogger(__name__) @@ -21,10 +21,20 @@ async def async_process_integration_platforms( async def _process(component_name: str) -> None: """Process the intents of a component.""" + if "." in component_name: + return + + integration = await async_get_integration(hass, component_name) + try: - integration = await async_get_integration(hass, component_name) platform = integration.get_platform(platform_name) - except (IntegrationNotFound, ImportError): + except ImportError as err: + if f"{component_name}.{platform_name}" not in str(err): + _LOGGER.exception( + "Unexpected error importing %s/%s.py", + component_name, + platform_name, + ) return try: diff --git a/tests/components/alexa/test_init.py b/tests/components/alexa/test_init.py index f5071cf3f01..605ca96f190 100644 --- a/tests/components/alexa/test_init.py +++ b/tests/components/alexa/test_init.py @@ -8,7 +8,9 @@ from tests.components.logbook.test_init import MockLazyEventPartialState async def test_humanify_alexa_event(hass): """Test humanifying Alexa event.""" + hass.config.components.add("recorder") await async_setup_component(hass, "alexa", {}) + await async_setup_component(hass, "logbook", {}) hass.states.async_set("light.kitchen", "on", {"friendly_name": "Kitchen Light"}) entity_attr_cache = logbook.EntityAttributeCache(hass) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index c41eb80d6f2..9af8a6591d9 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1039,7 +1039,9 @@ async def test_extraction_functions(hass): async def test_logbook_humanify_automation_triggered_event(hass): """Test humanifying Automation Trigger event.""" + hass.config.components.add("recorder") await async_setup_component(hass, automation.DOMAIN, {}) + await async_setup_component(hass, "logbook", {}) entity_attr_cache = logbook.EntityAttributeCache(hass) event1, event2 = list( diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 4db72ffb374..1fad563445b 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -15,8 +15,10 @@ from tests.components.logbook.test_init import MockLazyEventPartialState async def test_humanify_homekit_changed_event(hass, hk_driver): """Test humanifying HomeKit changed event.""" + hass.config.components.add("recorder") with patch("homeassistant.components.homekit.HomeKit"): assert await async_setup_component(hass, "homekit", {"homekit": {}}) + assert await async_setup_component(hass, "logbook", {}) entity_attr_cache = logbook.EntityAttributeCache(hass) event1, event2 = list( diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 58c918477f8..03ef09b438d 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -36,8 +36,8 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch -from tests.common import get_test_home_assistant, init_recorder_component +from tests.async_mock import Mock, patch +from tests.common import get_test_home_assistant, init_recorder_component, mock_platform from tests.components.recorder.common import trigger_db_commit _LOGGER = logging.getLogger(__name__) @@ -1563,6 +1563,22 @@ async def test_logbook_view_period_entity(hass, hass_client): async def test_logbook_describe_event(hass, hass_client): """Test teaching logbook about a new event.""" await hass.async_add_executor_job(init_recorder_component, hass) + + def _describe(event): + """Describe an event.""" + return {"name": "Test Name", "message": "tested a message"} + + hass.config.components.add("fake_integration") + mock_platform( + hass, + "fake_integration.logbook", + Mock( + async_describe_events=lambda hass, async_describe_event: async_describe_event( + "test_domain", "some_event", _describe + ) + ), + ) + assert await async_setup_component(hass, "logbook", {}) with patch( "homeassistant.util.dt.utcnow", @@ -1574,12 +1590,6 @@ async def test_logbook_describe_event(hass, hass_client): hass.data[recorder.DATA_INSTANCE].block_till_done ) - def _describe(event): - """Describe an event.""" - return {"name": "Test Name", "message": "tested a message"} - - hass.components.logbook.async_describe_event("test_domain", "some_event", _describe) - client = await hass_client() response = await client.get("/api/logbook") results = await response.json() @@ -1597,6 +1607,26 @@ async def test_exclude_described_event(hass, hass_client): entity_id2 = "automation.included_rule" entity_id3 = "sensor.excluded_domain" + def _describe(event): + """Describe an event.""" + return { + "name": "Test Name", + "message": "tested a message", + "entity_id": event.data.get(ATTR_ENTITY_ID), + } + + def async_describe_events(hass, async_describe_event): + """Mock to describe events.""" + async_describe_event("automation", "some_automation_event", _describe) + async_describe_event("sensor", "some_event", _describe) + + hass.config.components.add("fake_integration") + mock_platform( + hass, + "fake_integration.logbook", + Mock(async_describe_events=async_describe_events), + ) + await hass.async_add_executor_job(init_recorder_component, hass) assert await async_setup_component( hass, @@ -1631,19 +1661,6 @@ async def test_exclude_described_event(hass, hass_client): hass.data[recorder.DATA_INSTANCE].block_till_done ) - def _describe(event): - """Describe an event.""" - return { - "name": "Test Name", - "message": "tested a message", - "entity_id": event.data.get(ATTR_ENTITY_ID), - } - - hass.components.logbook.async_describe_event( - "automation", "some_automation_event", _describe - ) - hass.components.logbook.async_describe_event("sensor", "some_event", _describe) - client = await hass_client() response = await client.get("/api/logbook") results = await response.json() diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 9bcf0dc1be8..bb7340a08da 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -472,7 +472,9 @@ async def test_config(hass): async def test_logbook_humanify_script_started_event(hass): """Test humanifying script started event.""" + hass.config.components.add("recorder") await async_setup_component(hass, DOMAIN, {}) + await async_setup_component(hass, "logbook", {}) entity_attr_cache = logbook.EntityAttributeCache(hass) event1, event2 = list( From f4528d0db288e7b908ae9ec685e12c19e63d93ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Jun 2020 22:43:08 -0500 Subject: [PATCH 007/428] Ensure history states can be copied (#37081) The filter integration makes a copy of a state object obtained from history. --- homeassistant/components/history/__init__.py | 14 ++++++-- tests/components/history/test_init.py | 34 ++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 9b81ffa08ed..f943c126d3e 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -616,7 +616,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property + @property # type: ignore def attributes(self): """State attributes.""" if not self._attributes: @@ -628,13 +628,23 @@ class LazyState(State): self._attributes = {} return self._attributes - @property + @attributes.setter + def attributes(self, value): + """Set attributes.""" + self._attributes = value + + @property # type: ignore def context(self): """State context.""" if not self._context: self._context = Context(id=None) return self._context + @context.setter + def context(self, value): + """Set context.""" + self._context = value + @property # type: ignore def last_changed(self): """Last changed datetime.""" diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 89e16ad0205..34b22481400 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,5 +1,6 @@ """The tests the History component.""" # pylint: disable=protected-access,invalid-name +from copy import copy from datetime import timedelta import json import unittest @@ -188,6 +189,39 @@ class TestComponentHistory(unittest.TestCase): assert states == hist[entity_id] + def test_ensure_state_can_be_copied(self): + """Ensure a state can pass though copy(). + + The filter integration uses copy() on states + from history. + """ + self.init_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + self.hass.states.set(entity_id, state) + wait_recording_done(self.hass) + return self.hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=start + ): + set_state("1") + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=point + ): + set_state("2") + + hist = history.get_last_state_changes(self.hass, 2, entity_id) + + assert copy(hist[entity_id][0]) == hist[entity_id][0] + assert copy(hist[entity_id][1]) == hist[entity_id][1] + def test_get_significant_states(self): """Test that only significant states are returned. From 9137957cf9af28ef91cd1d0629b21f84b0ce29fe Mon Sep 17 00:00:00 2001 From: RogerSelwyn Date: Thu, 25 Jun 2020 07:20:00 +0100 Subject: [PATCH 008/428] Fix geniushub spamming log with exceptions (#37067) --- homeassistant/components/geniushub/__init__.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 0b99224bf7f..16967fb265a 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -167,6 +167,7 @@ class GeniusBroker: self.hass = hass self.client = client self._hub_uid = hub_uid + self._connect_error = False @property def hub_uid(self) -> int: @@ -178,8 +179,19 @@ class GeniusBroker: """Update the geniushub client's data.""" try: await self.client.update() - except aiohttp.ClientResponseError as err: - _LOGGER.warning("Update failed, message is: %s", err) + if self._connect_error: + self._connect_error = False + _LOGGER.warning("Connection to geniushub re-established") + except ( + aiohttp.ClientResponseError, + aiohttp.client_exceptions.ClientConnectorError, + ) as err: + if not self._connect_error: + self._connect_error = True + _LOGGER.warning( + "Connection to geniushub failed (unable to update), message is: %s", + err, + ) return self.make_debug_log_entries() @@ -240,7 +252,6 @@ class GeniusDevice(GeniusEntity): @property def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - attrs = {} attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] if self._last_comms: From c4235018040eb0febd188a5492a2534f0d189398 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 25 Jun 2020 01:20:39 -0500 Subject: [PATCH 009/428] Add legacy polling option for Amcrest motion detection (#36955) --- homeassistant/components/amcrest/__init__.py | 6 +-- .../components/amcrest/binary_sensor.py | 39 +++++++++++++++---- homeassistant/components/amcrest/const.py | 2 +- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index be2a6b78f30..3baad1ac88e 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_s from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.service import async_extract_entity_ids -from .binary_sensor import BINARY_SENSORS +from .binary_sensor import BINARY_POLLED_SENSORS, BINARY_SENSORS, check_binary_sensors from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST from .const import ( CAMERAS, @@ -98,7 +98,7 @@ AMCREST_SCHEMA = vol.Schema( vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)], vol.Unique() + cv.ensure_list, [vol.In(BINARY_SENSORS)], vol.Unique(), check_binary_sensors ), vol.Optional(CONF_SENSORS): vol.All( cv.ensure_list, [vol.In(SENSORS)], vol.Unique() @@ -271,7 +271,7 @@ def setup(hass, config): event_codes = [ BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] for sensor_type in binary_sensors - if BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] is not None + if sensor_type not in BINARY_POLLED_SENSORS ] if event_codes: _start_event_monitor(hass, name, api, event_codes) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index a3057211f2a..cfb61799be8 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from amcrest import AmcrestError +import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -12,6 +13,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util import Throttle from .const import ( BINARY_SENSOR_SCAN_INTERVAL_SECS, @@ -28,25 +30,42 @@ from .helpers import log_update_error, service_signal _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS) +_ONLINE_SCAN_INTERVAL = timedelta(seconds=60 - BINARY_SENSOR_SCAN_INTERVAL_SECS) BINARY_SENSOR_MOTION_DETECTED = "motion_detected" +BINARY_SENSOR_MOTION_DETECTED_POLLED = "motion_detected_polled" BINARY_SENSOR_ONLINE = "online" +BINARY_POLLED_SENSORS = [ + BINARY_SENSOR_MOTION_DETECTED_POLLED, + BINARY_SENSOR_ONLINE, +] +_MOTION_DETECTED_PARAMS = ("Motion Detected", DEVICE_CLASS_MOTION, "VideoMotion") BINARY_SENSORS = { - BINARY_SENSOR_MOTION_DETECTED: ( - "Motion Detected", - DEVICE_CLASS_MOTION, - "VideoMotion", - ), + BINARY_SENSOR_MOTION_DETECTED: _MOTION_DETECTED_PARAMS, + BINARY_SENSOR_MOTION_DETECTED_POLLED: _MOTION_DETECTED_PARAMS, BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY, None), } BINARY_SENSORS = { k: dict(zip((SENSOR_NAME, SENSOR_DEVICE_CLASS, SENSOR_EVENT_CODE), v)) for k, v in BINARY_SENSORS.items() } +_EXCLUSIVE_OPTIONS = [ + {BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSOR_MOTION_DETECTED_POLLED}, +] _UPDATE_MSG = "Updating %s binary sensor" +def check_binary_sensors(value): + """Validate binary sensor configurations.""" + for exclusive_options in _EXCLUSIVE_OPTIONS: + if len(set(value) & exclusive_options) > 1: + raise vol.Invalid( + f"must contain at most one of {', '.join(exclusive_options)}." + ) + return value + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a binary sensor for an Amcrest IP Camera.""" if discovery_info is None: @@ -80,7 +99,7 @@ class AmcrestBinarySensor(BinarySensorEntity): @property def should_poll(self): """Return True if entity has to be polled for state.""" - return self._sensor_type == BINARY_SENSOR_ONLINE + return self._sensor_type in BINARY_POLLED_SENSORS @property def name(self): @@ -109,6 +128,7 @@ class AmcrestBinarySensor(BinarySensorEntity): else: self._update_others() + @Throttle(_ONLINE_SCAN_INTERVAL) def _update_online(self): if not (self._api.available or self.is_on): return @@ -137,6 +157,11 @@ class AmcrestBinarySensor(BinarySensorEntity): async def async_on_demand_update(self): """Update state.""" + if self._sensor_type == BINARY_SENSOR_ONLINE: + _LOGGER.debug(_UPDATE_MSG, self._name) + self._state = self._api.available + self.async_write_ha_state() + return self.async_schedule_update_ha_state(True) @callback @@ -155,7 +180,7 @@ class AmcrestBinarySensor(BinarySensorEntity): self.async_on_demand_update, ) ) - if self._event_code: + if self._event_code and self._sensor_type not in BINARY_POLLED_SENSORS: self._unsub_dispatcher.append( async_dispatcher_connect( self.hass, diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index da7e5456786..ba7597d61af 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -4,7 +4,7 @@ DATA_AMCREST = DOMAIN CAMERAS = "cameras" DEVICES = "devices" -BINARY_SENSOR_SCAN_INTERVAL_SECS = 60 +BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 CAMERA_WEB_SESSION_TIMEOUT = 10 COMM_RETRIES = 1 COMM_TIMEOUT = 6.05 From fea5d007fc20dbc64b25fc0c4a6f05a0f57b8720 Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Thu, 25 Jun 2020 07:20:42 -0400 Subject: [PATCH 010/428] Clean up plum_lightpad (#37077) --- homeassistant/components/plum_lightpad/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 4a02e83de76..a94014ff1f9 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -1,4 +1,5 @@ """Support for Plum Lightpad lights.""" +import asyncio import logging from typing import Callable, List @@ -52,7 +53,7 @@ async def async_setup_entry( setup_entities(device) device_web_session = async_get_clientsession(hass, verify_ssl=False) - hass.loop.create_task( + asyncio.create_task( plum.discover( hass.loop, loadListener=new_load, From 140fc48ede5185df857d173c1316bcd96a84d05b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Jun 2020 14:17:31 +0200 Subject: [PATCH 011/428] Remove invalidation version from ZHA deprecated config options (#37089) --- homeassistant/components/zha/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 8a23c6fc20d..d5f76fa5e23 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -51,9 +51,9 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( vol.All( - cv.deprecated(CONF_USB_PATH, invalidation_version="0.112"), - cv.deprecated(CONF_BAUDRATE, invalidation_version="0.112"), - cv.deprecated(CONF_RADIO_TYPE, invalidation_version="0.112"), + cv.deprecated(CONF_USB_PATH), + cv.deprecated(CONF_BAUDRATE), + cv.deprecated(CONF_RADIO_TYPE), ZHA_CONFIG_SCHEMA, ), ), From 25b093e69e9939c131f4dc83566a9571929803df Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 25 Jun 2020 09:35:48 -0400 Subject: [PATCH 012/428] Use cached values for divisor/multiplier (#37070) --- homeassistant/components/zha/core/channels/homeautomation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 2601cf47573..d95180ce780 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -75,7 +75,7 @@ class ElectricalMeasurementChannel(ZigbeeChannel): async def async_initialize(self, from_cache): """Initialize channel.""" - await self.fetch_config(from_cache) + await self.fetch_config(True) await super().async_initialize(from_cache) async def fetch_config(self, from_cache): From 4aedafc73af5f7679b0544d77726be8192c5462a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Jun 2020 11:34:47 -0700 Subject: [PATCH 013/428] Improve setup (#37075) --- script/bootstrap | 3 ++- script/setup | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 211f1355b7d..2b599950625 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -7,4 +7,5 @@ set -e cd "$(dirname "$0")/.." echo "Installing development dependencies..." -python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) +python3 -m pip install wheel --constraint homeassistant/package_constraints.txt +python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) --constraint homeassistant/package_constraints.txt diff --git a/script/setup b/script/setup index 0076d70a7f0..eb7bda18d44 100755 --- a/script/setup +++ b/script/setup @@ -1,11 +1,26 @@ -#!/bin/sh +#!/bin/bash # Setups the repository. # Stop on errors set -e cd "$(dirname "$0")/.." + +mkdir -p config + +python3 -m venv venv +source venv/bin/activate + script/bootstrap pre-commit install -pip install -e . +pip install -e . --constraint homeassistant/package_constraints.txt + +hass --script ensure_config -c config + +echo " +logger: + default: info + logs: + homeassistant.components.cloud: debug +" >> config/configuration.yaml From 15773cb3e0ed96ef0a5d582ac0558b99d6df2dfa Mon Sep 17 00:00:00 2001 From: Indu Prakash <6459774+InduPrakash@users.noreply.github.com> Date: Thu, 25 Jun 2020 13:41:53 -0500 Subject: [PATCH 014/428] Add worldclock custom format (#36157) --- homeassistant/components/worldclock/sensor.py | 16 +++++++----- tests/components/worldclock/test_sensor.py | 26 ++++++++++++++++--- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 1709ac7d23e..86daa6e994a 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -9,18 +9,19 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util +CONF_TIME_FORMAT = "time_format" + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Worldclock Sensor" - ICON = "mdi:clock" - -TIME_STR_FORMAT = "%H:%M" +DEFAULT_TIME_STR_FORMAT = "%H:%M" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_TIME_ZONE): cv.time_zone, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TIME_FORMAT, default=DEFAULT_TIME_STR_FORMAT): cv.string, } ) @@ -30,17 +31,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config.get(CONF_TIME_ZONE)) - async_add_entities([WorldClockSensor(time_zone, name)], True) + async_add_entities( + [WorldClockSensor(time_zone, name, config.get(CONF_TIME_FORMAT),)], True, + ) class WorldClockSensor(Entity): """Representation of a World clock sensor.""" - def __init__(self, time_zone, name): + def __init__(self, time_zone, name, time_format): """Initialize the sensor.""" self._name = name self._time_zone = time_zone self._state = None + self._time_format = time_format @property def name(self): @@ -59,4 +63,4 @@ class WorldClockSensor(Entity): async def async_update(self): """Get the time and updates the states.""" - self._state = dt_util.now(time_zone=self._time_zone).strftime(TIME_STR_FORMAT) + self._state = dt_util.now(time_zone=self._time_zone).strftime(self._time_format) diff --git a/tests/components/worldclock/test_sensor.py b/tests/components/worldclock/test_sensor.py index 783ca41afff..e029ce783d3 100644 --- a/tests/components/worldclock/test_sensor.py +++ b/tests/components/worldclock/test_sensor.py @@ -15,15 +15,35 @@ class TestWorldClockSensor(unittest.TestCase): self.hass = get_test_home_assistant() self.time_zone = dt_util.get_time_zone("America/New_York") + def test_time(self): + """Test the time at a different location.""" config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}} - assert setup_component(self.hass, "sensor", config) self.hass.block_till_done() self.addCleanup(self.hass.stop) - def test_time(self): - """Test the time at a different location.""" state = self.hass.states.get("sensor.worldclock_sensor") assert state is not None assert state.state == dt_util.now(time_zone=self.time_zone).strftime("%H:%M") + + def test_time_format(self): + """Test time_format setting.""" + time_format = "%a, %b %d, %Y %I:%M %p" + config = { + "sensor": { + "platform": "worldclock", + "time_zone": "America/New_York", + "time_format": time_format, + } + } + assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() + self.addCleanup(self.hass.stop) + + state = self.hass.states.get("sensor.worldclock_sensor") + assert state is not None + + assert state.state == dt_util.now(time_zone=self.time_zone).strftime( + time_format + ) From 2dd0a182abe64c4d4f9b1573b53f88a87889388c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Jun 2020 20:42:33 +0200 Subject: [PATCH 015/428] Bump version to 0.113.0dev0 (#37071) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 900df1f9336..fe90ccf8504 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 112 +MINOR_VERSION = 113 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 6d9fa34f650d687cde4a68bed82a333d800663bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jun 2020 15:17:05 -0500 Subject: [PATCH 016/428] Migrate doorbird to use new logbook platform (#37097) --- homeassistant/components/doorbird/__init__.py | 9 ++--- homeassistant/components/doorbird/camera.py | 35 +++++++++++++++++-- homeassistant/components/doorbird/const.py | 2 ++ homeassistant/components/doorbird/logbook.py | 33 +++++++++++++++++ .../components/doorbird/manifest.json | 2 +- 5 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/doorbird/logbook.py diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 048cd87c3aa..b4af58a740f 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -9,7 +9,6 @@ from doorbirdpy import DoorBird import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.logbook import log_entry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_DEVICES, @@ -165,6 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + unload_ok = all( await asyncio.gather( *[ @@ -228,6 +228,7 @@ class ConfiguredDoorBird: self._device = device self._custom_url = custom_url self.events = events + self.doorstation_events = [self._get_event_name(event) for event in self.events] self._token = token @property @@ -259,9 +260,7 @@ class ConfiguredDoorBird: if self.custom_url is not None: hass_url = self.custom_url - for event in self.events: - event = self._get_event_name(event) - + for event in self.doorstation_events: self._register_event(hass_url, event) _LOGGER.info("Successfully registered URL for %s on %s", event, self.name) @@ -365,6 +364,4 @@ class DoorBirdRequestView(HomeAssistantView): hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) - log_entry(hass, f"Doorbird {event}", "event was fired.", DOMAIN) - return web.Response(status=HTTP_OK, text="OK") diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index bf999489589..53fcdbcee70 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -10,7 +10,12 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util -from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO +from .const import ( + DOMAIN, + DOOR_STATION, + DOOR_STATION_EVENT_ENTITY_IDS, + DOOR_STATION_INFO, +) from .entity import DoorBirdEntity _LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=2) @@ -23,8 +28,9 @@ _TIMEOUT = 15 # seconds async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the DoorBird camera platform.""" config_entry_id = config_entry.entry_id - doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] - doorstation_info = hass.data[DOMAIN][config_entry_id][DOOR_STATION_INFO] + config_data = hass.data[DOMAIN][config_entry_id] + doorstation = config_data[DOOR_STATION] + doorstation_info = config_data[DOOR_STATION_INFO] device = doorstation.device async_add_entities( @@ -35,6 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device.live_image_url, "live", f"{doorstation.name} Live", + doorstation.doorstation_events, _LIVE_INTERVAL, device.rtsp_live_video_url, ), @@ -44,6 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device.history_image_url(1, "doorbell"), "last_ring", f"{doorstation.name} Last Ring", + [], _LAST_VISITOR_INTERVAL, ), DoorBirdCamera( @@ -52,6 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device.history_image_url(1, "motionsensor"), "last_motion", f"{doorstation.name} Last Motion", + [], _LAST_MOTION_INTERVAL, ), ] @@ -68,6 +77,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): url, camera_id, name, + doorstation_events, interval=None, stream_url=None, ): @@ -81,6 +91,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): self._interval = interval or datetime.timedelta self._last_update = datetime.datetime.min self._unique_id = f"{self._mac_addr}_{camera_id}" + self._doorstation_events = doorstation_events async def stream_source(self): """Return the stream source.""" @@ -124,3 +135,21 @@ class DoorBirdCamera(DoorBirdEntity, Camera): "DoorBird %s: Error getting camera image: %s", self._name, error ) return self._last_image + + async def async_added_to_hass(self): + """Add callback after being added to hass. + + Registers entity_id map for the logbook + """ + event_to_entity_id = self.hass.data[DOMAIN].setdefault( + DOOR_STATION_EVENT_ENTITY_IDS, {} + ) + for event in self._doorstation_events: + event_to_entity_id[event] = self.entity_id + + async def will_remove_from_hass(self): + """Unregister entity_id map for the logbook.""" + event_to_entity_id = self.hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS] + for event in self._doorstation_events: + if event in event_to_entity_id: + del event_to_entity_id[event] diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py index 3b639fc8dca..af847dac673 100644 --- a/homeassistant/components/doorbird/const.py +++ b/homeassistant/components/doorbird/const.py @@ -5,6 +5,8 @@ DOMAIN = "doorbird" PLATFORMS = ["switch", "camera"] DOOR_STATION = "door_station" DOOR_STATION_INFO = "door_station_info" +DOOR_STATION_EVENT_ENTITY_IDS = "door_station_event_entity_ids" + CONF_EVENTS = "events" MANUFACTURER = "Bird Home Automation Group" DOORBIRD_OUI = "1CCAE3" diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py new file mode 100644 index 00000000000..ebe28cd350f --- /dev/null +++ b/homeassistant/components/doorbird/logbook.py @@ -0,0 +1,33 @@ +"""Describe logbook events.""" + +from homeassistant.core import callback + +from .const import DOMAIN, DOOR_STATION, DOOR_STATION_EVENT_ENTITY_IDS + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + _, doorbird_event = event.event_type.split("_", 1) + + return { + "name": "Doorbird", + "message": f"Event {event.event_type} was fired.", + "entity_id": hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS].get( + doorbird_event + ), + } + + domain_data = hass.data[DOMAIN] + + for config_entry_id in domain_data: + door_station = domain_data[config_entry_id][DOOR_STATION] + + for event in door_station.doorstation_events: + async_describe_event( + DOMAIN, f"{DOMAIN}_{event}", async_describe_logbook_event + ) diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 6c1c75ff328..58311fa65e4 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -3,7 +3,7 @@ "name": "DoorBird", "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": ["doorbirdpy==2.0.8"], - "dependencies": ["http", "logbook"], + "dependencies": ["http"], "zeroconf": ["_axis-video._tcp.local."], "codeowners": ["@oblogic7", "@bdraco"], "config_flow": true From 98a388e65ac5f8ce8cd0ca80b4da9edba6566cdd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 26 Jun 2020 00:04:23 +0000 Subject: [PATCH 017/428] [ci skip] Translation update --- .../humidifier/translations/zh-Hant.json | 18 ++++++++++++++++++ .../components/mqtt/translations/ru.json | 14 ++++++++++++-- .../plum_lightpad/translations/zh-Hant.json | 18 ++++++++++++++++++ .../components/toon/translations/zh-Hant.json | 14 ++++++++++++++ .../components/withings/translations/ru.json | 4 ++-- 5 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/humidifier/translations/zh-Hant.json create mode 100644 homeassistant/components/plum_lightpad/translations/zh-Hant.json diff --git a/homeassistant/components/humidifier/translations/zh-Hant.json b/homeassistant/components/humidifier/translations/zh-Hant.json new file mode 100644 index 00000000000..c067d97d956 --- /dev/null +++ b/homeassistant/components/humidifier/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "\u8a2d\u5b9a{entity_name}\u6fd5\u5ea6", + "set_mode": "\u8b8a\u66f4{entity_name}\u6a21\u5f0f", + "toggle": "\u5207\u63db{entity_name}", + "turn_off": "\u95dc\u9589{entity_name}", + "turn_on": "\u958b\u555f{entity_name}" + } + }, + "state": { + "_": { + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + } + }, + "title": "\u52a0\u6fd5\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 21f5e11322f..8139781f51e 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -50,6 +50,8 @@ }, "options": { "error": { + "bad_birth": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.", + "bad_will": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443" }, "step": { @@ -64,9 +66,17 @@ }, "options": { "data": { - "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435" + "birth_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "birth_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "birth_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "birth_topic": "\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)", + "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", + "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_topic": "\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MQTT." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MQTT." } } } diff --git a/homeassistant/components/plum_lightpad/translations/zh-Hant.json b/homeassistant/components/plum_lightpad/translations/zh-Hant.json new file mode 100644 index 00000000000..d32e8d686d4 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json index 618a3a5fd00..d827f288732 100644 --- a/homeassistant/components/toon/translations/zh-Hant.json +++ b/homeassistant/components/toon/translations/zh-Hant.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "\u6240\u9078\u64c7\u7684\u5354\u8b70\u5730\u5740\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", "client_id": "\u8a2d\u5b9a\u5167\u7528\u6236\u7aef ID \u7121\u6548\u3002", "client_secret": "\u8a2d\u5b9a\u5167\u5ba2\u6236\u7aef\u5bc6\u78bc\u7121\u6548\u3002", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u8a2d\u5099\u3002", "no_app": "\u5fc5\u9808\u5148\u8a2d\u5b9a Toon \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/toon/(\u3002", "unknown_auth_fail": "\u9a57\u8b49\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" @@ -12,6 +16,13 @@ "display_exists": "\u6240\u9078\u64c7\u7684\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" }, "step": { + "agreement": { + "data": { + "agreement": "\u5354\u8b70\u5730\u5740" + }, + "description": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684\u5354\u8b70\u5730\u5740\u3002", + "title": "\u9078\u64c7\u5354\u8b70\u5730\u5740" + }, "authenticate": { "data": { "password": "\u5bc6\u78bc", @@ -27,6 +38,9 @@ }, "description": "\u9078\u64c7\u6240\u8981\u9023\u63a5\u7684 Toon display\u3002", "title": "\u9078\u64c7\u8a2d\u5099" + }, + "pick_implementation": { + "title": "\u9078\u64c7\u6240\u8981\u8a8d\u8b49\u7684\u5730\u5740" } } } diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index 33c2437df62..b26efaedb18 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -18,9 +18,9 @@ }, "profile": { "data": { - "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" + "profile": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044f" }, - "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.", + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445. \u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u044d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435, \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u043d\u0430 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u043c \u0448\u0430\u0433\u0435.", "title": "Withings" }, "reauth": { From f42eb0d5cadb2ebd2eeb5a5f1240fda9163da46c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 26 Jun 2020 02:09:52 +0200 Subject: [PATCH 018/428] Fix missing service call context in multiple locations (#37094) --- homeassistant/components/alert/__init__.py | 4 +++- .../components/generic_thermostat/climate.py | 8 +++++-- homeassistant/components/group/cover.py | 24 ++++++++++++------- homeassistant/components/group/light.py | 19 ++++++++++++--- .../components/homeassistant/__init__.py | 4 +++- homeassistant/components/lifx/light.py | 4 +++- homeassistant/components/switch/light.py | 12 ++++++++-- homeassistant/components/tts/__init__.py | 6 ++++- .../components/universal/media_player.py | 2 +- tests/components/group/test_light.py | 6 ++--- 10 files changed, 66 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 0ac4621cb0a..d85c13731b2 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -305,7 +305,9 @@ class Alert(ToggleEntity): _LOGGER.debug(msg_payload) for target in self._notifiers: - await self.hass.services.async_call(DOMAIN_NOTIFY, target, msg_payload) + await self.hass.services.async_call( + DOMAIN_NOTIFY, target, msg_payload, context=self._context + ) async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index d7889513402..407923dc161 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -449,12 +449,16 @@ class GenericThermostat(ClimateEntity, RestoreEntity): async def _async_heater_turn_on(self): """Turn heater toggleable device on.""" data = {ATTR_ENTITY_ID: self.heater_entity_id} - await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data) + await self.hass.services.async_call( + HA_DOMAIN, SERVICE_TURN_ON, data, context=self._context + ) async def _async_heater_turn_off(self): """Turn heater toggleable device off.""" data = {ATTR_ENTITY_ID: self.heater_entity_id} - await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data) + await self.hass.services.async_call( + HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context + ) async def async_set_preset_mode(self, preset_mode: str): """Set new preset mode.""" diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 2638ce072a3..427530dadb5 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -209,21 +209,21 @@ class CoverGroup(CoverEntity): """Move the covers up.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} await self.hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, data, blocking=True + DOMAIN, SERVICE_OPEN_COVER, data, blocking=True, context=self._context ) async def async_close_cover(self, **kwargs): """Move the covers down.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} await self.hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True + DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True, context=self._context ) async def async_stop_cover(self, **kwargs): """Fire the stop action.""" data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]} await self.hass.services.async_call( - DOMAIN, SERVICE_STOP_COVER, data, blocking=True + DOMAIN, SERVICE_STOP_COVER, data, blocking=True, context=self._context ) async def async_set_cover_position(self, **kwargs): @@ -233,28 +233,32 @@ class CoverGroup(CoverEntity): ATTR_POSITION: kwargs[ATTR_POSITION], } await self.hass.services.async_call( - DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True + DOMAIN, + SERVICE_SET_COVER_POSITION, + data, + blocking=True, + context=self._context, ) async def async_open_cover_tilt(self, **kwargs): """Tilt covers open.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} await self.hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True + DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True, context=self._context ) async def async_close_cover_tilt(self, **kwargs): """Tilt covers closed.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} await self.hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True + DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True, context=self._context ) async def async_stop_cover_tilt(self, **kwargs): """Stop cover tilt.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]} await self.hass.services.async_call( - DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True + DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True, context=self._context ) async def async_set_cover_tilt_position(self, **kwargs): @@ -264,7 +268,11 @@ class CoverGroup(CoverEntity): ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION], } await self.hass.services.async_call( - DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True + DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + data, + blocking=True, + context=self._context, ) async def async_update(self): diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 1136df7eac0..69329b96122 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -233,7 +233,11 @@ class LightGroup(light.LightEntity): if not emulate_color_temp_entity_ids: await self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + light.DOMAIN, + light.SERVICE_TURN_ON, + data, + blocking=True, + context=self._context, ) return @@ -249,13 +253,18 @@ class LightGroup(light.LightEntity): await asyncio.gather( self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + light.DOMAIN, + light.SERVICE_TURN_ON, + data, + blocking=True, + context=self._context, ), self.hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, emulate_color_temp_data, blocking=True, + context=self._context, ), ) @@ -267,7 +276,11 @@ class LightGroup(light.LightEntity): data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION] await self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_OFF, data, blocking=True + light.DOMAIN, + light.SERVICE_TURN_OFF, + data, + blocking=True, + context=self._context, ) async def async_update(self): diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index e0a4d88ec6a..83166ba4cce 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -78,7 +78,9 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: data[ATTR_ENTITY_ID] = list(ent_ids) tasks.append( - hass.services.async_call(domain, service.service, data, blocking) + hass.services.async_call( + domain, service.service, data, blocking, context=service.context + ) ) if tasks: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 2b7629cdaf2..26a2acfa517 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -641,7 +641,9 @@ class LIFXLight(LightEntity): """Start an effect with default parameters.""" service = kwargs[ATTR_EFFECT] data = {ATTR_ENTITY_ID: self.entity_id} - await self.hass.services.async_call(LIFX_DOMAIN, service, data) + await self.hass.services.async_call( + LIFX_DOMAIN, service, data, context=self._context + ) async def async_update(self): """Update bulb status.""" diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index f40ccde5b0b..c23390a3e3e 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -84,14 +84,22 @@ class LightSwitch(LightEntity): """Forward the turn_on command to the switch in this light switch.""" data = {ATTR_ENTITY_ID: self._switch_entity_id} await self.hass.services.async_call( - switch.DOMAIN, switch.SERVICE_TURN_ON, data, blocking=True + switch.DOMAIN, + switch.SERVICE_TURN_ON, + data, + blocking=True, + context=self._context, ) async def async_turn_off(self, **kwargs): """Forward the turn_off command to the switch in this light switch.""" data = {ATTR_ENTITY_ID: self._switch_entity_id} await self.hass.services.async_call( - switch.DOMAIN, switch.SERVICE_TURN_OFF, data, blocking=True + switch.DOMAIN, + switch.SERVICE_TURN_OFF, + data, + blocking=True, + context=self._context, ) async def async_update(self): diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index ebd7a1c8411..39e4702e855 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -175,7 +175,11 @@ async def async_setup(hass, config): } await hass.services.async_call( - DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True + DOMAIN_MP, + SERVICE_PLAY_MEDIA, + data, + blocking=True, + context=service.context, ) service_name = p_config.get(CONF_SERVICE_NAME, f"{p_type}_{SERVICE_SAY}") diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index f1cad7e8abf..ec4b53cd2e0 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -205,7 +205,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): service_data[ATTR_ENTITY_ID] = active_child.entity_id await self.hass.services.async_call( - DOMAIN, service_name, service_data, blocking=True + DOMAIN, service_name, service_data, blocking=True, context=self._context ) @property diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 1f68279ae05..2a2e21f77c5 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -570,14 +570,14 @@ async def test_invalid_service_calls(hass): await grouped_light.async_turn_on(brightness=150, four_oh_four="404") data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_BRIGHTNESS: 150} mock_call.assert_called_once_with( - LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None ) mock_call.reset_mock() await grouped_light.async_turn_off(transition=4, four_oh_four="404") data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_TRANSITION: 4} mock_call.assert_called_once_with( - LIGHT_DOMAIN, SERVICE_TURN_OFF, data, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_OFF, data, blocking=True, context=None ) mock_call.reset_mock() @@ -596,5 +596,5 @@ async def test_invalid_service_calls(hass): data.pop(ATTR_RGB_COLOR) data.pop(ATTR_XY_COLOR) mock_call.assert_called_once_with( - LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None ) From fd1a8dd96cd0a7e5f4111b72353f2915b766508c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 25 Jun 2020 20:10:40 -0400 Subject: [PATCH 019/428] Fix Plex when using local tokenless authentication (#37096) --- homeassistant/components/plex/__init__.py | 20 ++++-- homeassistant/components/plex/server.py | 52 ++++++++++---- tests/components/plex/test_config_flow.py | 14 ++-- tests/components/plex/test_init.py | 79 +++++++++++++++++---- tests/components/plex/test_media_players.py | 31 ++++---- tests/components/plex/test_playback.py | 8 +-- tests/components/plex/test_server.py | 37 +++++----- 7 files changed, 157 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 89a3570dd10..c83dfe13347 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -161,12 +161,20 @@ async def async_setup_entry(hass, entry): } ) - hass.services.async_register( - PLEX_DOMAIN, - SERVICE_PLAY_ON_SONOS, - async_play_on_sonos_service, - schema=play_on_sonos_schema, - ) + def get_plex_account(plex_server): + try: + return plex_server.account + except plexapi.exceptions.Unauthorized: + return None + + plex_account = await hass.async_add_executor_job(get_plex_account, plex_server) + if plex_account: + hass.services.async_register( + PLEX_DOMAIN, + SERVICE_PLAY_ON_SONOS, + async_play_on_sonos_service, + schema=play_on_sonos_schema, + ) return True diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 05dae668512..a49a73cb51b 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -76,6 +76,7 @@ class PlexServer: self._plextv_clients = None self._plextv_client_timestamp = 0 self._plextv_device_cache = {} + self._use_plex_tv = self._token is not None self._version = None self.async_update_platforms = Debouncer( hass, @@ -94,18 +95,35 @@ class PlexServer: @property def account(self): """Return a MyPlexAccount instance.""" - if not self._plex_account: - self._plex_account = plexapi.myplex.MyPlexAccount(token=self._token) + if not self._plex_account and self._use_plex_tv: + try: + self._plex_account = plexapi.myplex.MyPlexAccount(token=self._token) + except Unauthorized: + self._use_plex_tv = False + _LOGGER.error("Not authorized to access plex.tv with provided token") + raise return self._plex_account + @property + def plextv_resources(self): + """Return all resources linked to Plex account.""" + if self.account is None: + return [] + + return self.account.resources() + def plextv_clients(self): """Return available clients linked to Plex account.""" + if self.account is None: + return [] + now = time.time() if now - self._plextv_client_timestamp > PLEXTV_THROTTLE: self._plextv_client_timestamp = now - resources = self.account.resources() self._plextv_clients = [ - x for x in resources if "player" in x.provides and x.presence + x + for x in self.plextv_resources + if "player" in x.provides and x.presence ] _LOGGER.debug( "Current available clients from plex.tv: %s", self._plextv_clients @@ -119,7 +137,7 @@ class PlexServer: def _connect_with_token(): available_servers = [ (x.name, x.clientIdentifier) - for x in self.account.resources() + for x in self.plextv_resources if "server" in x.provides ] @@ -145,14 +163,18 @@ class PlexServer: ) def _update_plexdirect_hostname(): - matching_server = [ + matching_servers = [ x.name - for x in self.account.resources() + for x in self.plextv_resources if x.clientIdentifier == self._server_id - ][0] - self._plex_server = self.account.resource(matching_server).connect( - timeout=10 - ) + ] + if matching_servers: + self._plex_server = self.account.resource(matching_servers[0]).connect( + timeout=10 + ) + return True + _LOGGER.error("Attempt to update plex.direct hostname failed") + return False if self._url: try: @@ -168,8 +190,12 @@ class PlexServer: _LOGGER.warning( "Plex SSL certificate's hostname changed, updating." ) - _update_plexdirect_hostname() - config_entry_update_needed = True + if _update_plexdirect_hostname(): + config_entry_update_needed = True + else: + raise Unauthorized( + "New certificate cannot be validated with provided token" + ) else: raise else: diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index f218b4c4d79..125367a32f6 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -367,8 +367,8 @@ async def test_option_flow(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -417,8 +417,8 @@ async def test_missing_option_flow(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -471,8 +471,8 @@ async def test_option_flow_new_users_available(hass, caplog): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -741,6 +741,8 @@ async def test_setup_with_limited_credentials(hass): ), patch.object( mock_plex_server, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized ) as mock_accounts, patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch( "homeassistant.components.plex.PlexWebsocket.listen" ) as mock_listen: entry.add_to_hass(hass) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 5f626bf6a23..76b1138fc06 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.const import CONF_URL, CONF_VERIFY_SSL +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.util.dt as dt_util @@ -115,8 +115,8 @@ async def test_set_config_entry_unique_id(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -181,8 +181,8 @@ async def test_setup_with_insecure_config_entry(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -210,8 +210,8 @@ async def test_unload_config_entry(hass): assert entry is config_entries[0] with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert mock_listen.called @@ -243,8 +243,8 @@ async def test_setup_with_photo_session(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -254,11 +254,8 @@ async def test_setup_with_photo_session(hass): server_id = mock_plex_server.machineIdentifier - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() + async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() media_player = hass.states.get("media_player.plex_product_title") assert media_player.state == "idle" @@ -293,10 +290,33 @@ async def test_setup_when_certificate_changed(hass): new_entry = MockConfigEntry(domain=const.DOMAIN, data=DEFAULT_DATA) + # Test with account failure + with patch( + "plexapi.server.PlexServer", side_effect=WrongCertHostnameException + ), patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + ): + old_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(old_entry.entry_id) is False + await hass.async_block_till_done() + + assert old_entry.state == ENTRY_STATE_SETUP_ERROR + await hass.config_entries.async_unload(old_entry.entry_id) + + # Test with no servers found + with patch( + "plexapi.server.PlexServer", side_effect=WrongCertHostnameException + ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0)): + assert await hass.config_entries.async_setup(old_entry.entry_id) is False + await hass.async_block_till_done() + + assert old_entry.state == ENTRY_STATE_SETUP_ERROR + await hass.config_entries.async_unload(old_entry.entry_id) + + # Test with success with patch( "plexapi.server.PlexServer", side_effect=WrongCertHostnameException ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - old_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(old_entry.entry_id) await hass.async_block_till_done() @@ -307,3 +327,32 @@ async def test_setup_when_certificate_changed(hass): old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] == new_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] ) + + +async def test_tokenless_server(hass): + """Test setup with a server with token auth disabled.""" + mock_plex_server = MockPlexServer() + + TOKENLESS_DATA = copy.deepcopy(DEFAULT_DATA) + TOKENLESS_DATA[const.PLEX_SERVER_CONFIG].pop(CONF_TOKEN, None) + + entry = MockConfigEntry( + domain=const.DOMAIN, + data=TOKENLESS_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_LOADED + + server_id = mock_plex_server.machineIdentifier + + async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index 0cd76c15ab6..d3e2de91cf9 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -23,8 +23,8 @@ async def test_plex_tv_clients(hass): mock_plex_account = MockPlexAccount() with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -37,9 +37,7 @@ async def test_plex_tv_clients(hass): for x in mock_plex_account.resources() if x.name.startswith("plex.tv Resource Player") ) - with patch( - "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account - ), patch.object(resource, "connect", side_effect=NotFound): + with patch.object(resource, "connect", side_effect=NotFound): await plex_server._async_update_platforms() await hass.async_block_till_done() @@ -49,16 +47,15 @@ async def test_plex_tv_clients(hass): await hass.config_entries.async_unload(entry.entry_id) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() plex_server = hass.data[DOMAIN][SERVERS][server_id] - with patch("plexapi.myplex.MyPlexAccount", return_value=mock_plex_account): - await plex_server._async_update_platforms() - await hass.async_block_till_done() + await plex_server._async_update_platforms() + await hass.async_block_till_done() media_players_after = len(hass.states.async_entity_ids("media_player")) assert media_players_after == media_players_before + 1 @@ -70,22 +67,20 @@ async def test_plex_tv_clients(hass): mock_plex_server.clear_sessions() with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() plex_server = hass.data[DOMAIN][SERVERS][server_id] - with patch("plexapi.myplex.MyPlexAccount", return_value=mock_plex_account): - await plex_server._async_update_platforms() - await hass.async_block_till_done() + await plex_server._async_update_platforms() + await hass.async_block_till_done() assert len(hass.states.async_entity_ids("media_player")) == 1 # Ensure cache gets called - with patch("plexapi.myplex.MyPlexAccount", return_value=mock_plex_account): - await plex_server._async_update_platforms() - await hass.async_block_till_done() + await plex_server._async_update_platforms() + await hass.async_block_till_done() assert len(hass.states.async_entity_ids("media_player")) == 1 diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 7a90d8dfad8..dafc8720ab1 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -28,8 +28,8 @@ async def test_sonos_playback(hass): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -37,10 +37,6 @@ async def test_sonos_playback(hass): server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - # Access and cache PlexAccount - assert loaded_server.account - # Test Sonos integration lookup failure with patch.object( hass.components.sonos, "get_coordinator_id", side_effect=HomeAssistantError diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index a42e1aff710..5cd0d13e90c 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -55,17 +55,16 @@ async def test_new_users_available(hass): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() server_id = mock_plex_server.machineIdentifier - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -95,17 +94,16 @@ async def test_new_ignored_users_available(hass, caplog): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() server_id = mock_plex_server.machineIdentifier - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -248,17 +246,16 @@ async def test_ignore_plex_web_client(hass): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0) + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() server_id = mock_plex_server.machineIdentifier - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)): - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -281,8 +278,8 @@ async def test_media_lookups(hass): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -291,9 +288,9 @@ async def test_media_lookups(hass): loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Plex Key searches - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + media_player_id = hass.states.async_entity_ids("media_player")[0] with patch("homeassistant.components.plex.PlexServer.create_playqueue"): assert await hass.services.async_call( From 30df871787976ef76f529575f44e356e07d44e2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jun 2020 19:11:49 -0500 Subject: [PATCH 020/428] Improve isoformat timestamp performance for full states (#37105) --- homeassistant/components/history/__init__.py | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index f943c126d3e..0d127444d88 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -669,6 +669,33 @@ class LazyState(State): """Set last updated datetime.""" self._last_updated = value + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + def __eq__(self, other): """Return the comparison.""" return ( From 56907392d3ad76d5aa71ac38a6397530b4d028e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Jun 2020 17:38:09 -0700 Subject: [PATCH 021/428] Bump frontend (#37113) --- homeassistant/components/frontend/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 5427932774d..546ca70cff3 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20200623.3" - ], + "requirements": ["home-assistant-frontend==20200626.0"], "dependencies": [ "api", "auth", @@ -17,8 +15,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 26232031566..824a1fe003a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.34.7 -home-assistant-frontend==20200623.3 +home-assistant-frontend==20200626.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 netdisco==2.7.1 diff --git a/requirements_all.txt b/requirements_all.txt index 1b21e45c71b..98a82483dc6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200623.3 +home-assistant-frontend==20200626.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c8cdba6c5f..eb0dd8f49f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200623.3 +home-assistant-frontend==20200626.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 40573bf393107b4bec399b5e326f0b2acea2cbb2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 26 Jun 2020 02:24:58 -0400 Subject: [PATCH 022/428] Plex tests cleanup and additions (#37117) --- homeassistant/components/plex/server.py | 14 +- tests/components/plex/helpers.py | 9 ++ tests/components/plex/test_config_flow.py | 5 +- tests/components/plex/test_init.py | 115 ++++------------ tests/components/plex/test_server.py | 155 +++++++++------------- 5 files changed, 102 insertions(+), 196 deletions(-) create mode 100644 tests/components/plex/helpers.py diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index a49a73cb51b..1a32e08bcf6 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -104,14 +104,6 @@ class PlexServer: raise return self._plex_account - @property - def plextv_resources(self): - """Return all resources linked to Plex account.""" - if self.account is None: - return [] - - return self.account.resources() - def plextv_clients(self): """Return available clients linked to Plex account.""" if self.account is None: @@ -122,7 +114,7 @@ class PlexServer: self._plextv_client_timestamp = now self._plextv_clients = [ x - for x in self.plextv_resources + for x in self.account.resources() if "player" in x.provides and x.presence ] _LOGGER.debug( @@ -137,7 +129,7 @@ class PlexServer: def _connect_with_token(): available_servers = [ (x.name, x.clientIdentifier) - for x in self.plextv_resources + for x in self.account.resources() if "server" in x.provides ] @@ -165,7 +157,7 @@ class PlexServer: def _update_plexdirect_hostname(): matching_servers = [ x.name - for x in self.plextv_resources + for x in self.account.resources() if x.clientIdentifier == self._server_id ] if matching_servers: diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py new file mode 100644 index 00000000000..462bd2a5061 --- /dev/null +++ b/tests/components/plex/helpers.py @@ -0,0 +1,9 @@ +"""Helper methods for Plex tests.""" +from homeassistant.components.plex.const import DOMAIN, WEBSOCKETS + + +def trigger_plex_update(hass, plex_server): + """Call the websocket callback method.""" + server_id = plex_server.machineIdentifier + websocket = hass.data[DOMAIN][WEBSOCKETS][server_id] + websocket.callback() diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 125367a32f6..87183c8f2ad 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -18,7 +18,6 @@ from homeassistant.components.plex.const import ( DOMAIN, MANUAL_SETUP_STRING, PLEX_SERVER_CONFIG, - PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) from homeassistant.config import async_process_ha_core_config @@ -34,9 +33,9 @@ from homeassistant.const import ( CONF_URL, CONF_VERIFY_SSL, ) -from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN +from .helpers import trigger_plex_update from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer, MockResource from tests.async_mock import patch @@ -480,7 +479,7 @@ async def test_option_flow_new_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + trigger_plex_update(hass, mock_plex_server) await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 76b1138fc06..6902fb4401f 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -14,96 +14,15 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL -from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.util.dt as dt_util from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .helpers import trigger_plex_update from .mock_classes import MockPlexAccount, MockPlexServer from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed -# class TestClockedPlex(ClockedTestCase): -# """Create clock-controlled tests.async_mock class.""" - -# @pytest.fixture(autouse=True) -# def inject_fixture(self, caplog, hass_storage): -# """Inject pytest fixtures as instance attributes.""" -# self.caplog = caplog - -# async def setUp(self): -# """Initialize this test class.""" -# self.hass = await async_test_home_assistant(self.loop) - -# async def tearDown(self): -# """Clean up the HomeAssistant instance.""" -# await self.hass.async_stop() - -# async def test_setup_with_config_entry(self): -# """Test setup component with config.""" -# hass = self.hass - -# mock_plex_server = MockPlexServer() - -# entry = MockConfigEntry( -# domain=const.DOMAIN, -# data=DEFAULT_DATA, -# options=DEFAULT_OPTIONS, -# unique_id=DEFAULT_DATA["server_id"], -# ) - -# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( -# "homeassistant.components.plex.PlexWebsocket.listen" -# ) as mock_listen: -# entry.add_to_hass(hass) -# assert await hass.config_entries.async_setup(entry.entry_id) -# await hass.async_block_till_done() - -# assert mock_listen.called - -# assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 -# assert entry.state == ENTRY_STATE_LOADED - -# server_id = mock_plex_server.machineIdentifier -# loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - -# assert loaded_server.plex_server == mock_plex_server - -# async_dispatcher_send( -# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) -# ) -# await hass.async_block_till_done() - -# sensor = hass.states.get("sensor.plex_plex_server_1") -# assert sensor.state == str(len(mock_plex_server.accounts)) - -# # Ensure existing entities refresh -# await self.advance(const.DEBOUNCE_TIMEOUT) -# async_dispatcher_send( -# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) -# ) -# await hass.async_block_till_done() - -# for test_exception in ( -# plexapi.exceptions.BadRequest, -# requests.exceptions.RequestException, -# ): -# with patch.object( -# mock_plex_server, "clients", side_effect=test_exception -# ) as patched_clients_bad_request: -# await self.advance(const.DEBOUNCE_TIMEOUT) -# async_dispatcher_send( -# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) -# ) -# await hass.async_block_till_done() - -# assert patched_clients_bad_request.called -# assert ( -# f"Could not connect to Plex server: {mock_plex_server.friendlyName}" -# in self.caplog.text -# ) -# self.caplog.clear() - async def test_set_config_entry_unique_id(hass): """Test updating missing unique_id from config entry.""" @@ -252,9 +171,7 @@ async def test_setup_with_photo_session(hass): assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED - server_id = mock_plex_server.machineIdentifier - - async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + trigger_plex_update(hass, mock_plex_server) await hass.async_block_till_done() media_player = hass.states.get("media_player.plex_product_title") @@ -343,6 +260,30 @@ async def test_tokenless_server(hass): unique_id=DEFAULT_DATA["server_id"], ) + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_LOADED + + trigger_plex_update(hass, mock_plex_server) + await hass.async_block_till_done() + + +async def test_bad_token_with_tokenless_server(hass): + """Test setup with a bad token and a server with token auth disabled.""" + mock_plex_server = MockPlexServer() + + entry = MockConfigEntry( + domain=const.DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), patch("homeassistant.components.plex.PlexWebsocket.listen"): @@ -352,7 +293,5 @@ async def test_tokenless_server(hass): assert entry.state == ENTRY_STATE_LOADED - server_id = mock_plex_server.machineIdentifier - - async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + trigger_plex_update(hass, mock_plex_server) await hass.async_block_till_done() diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 5cd0d13e90c..f678fb30183 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -2,6 +2,7 @@ import copy from plexapi.exceptions import NotFound +from requests.exceptions import RequestException from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player.const import ( @@ -17,14 +18,14 @@ from homeassistant.components.plex.const import ( CONF_IGNORE_NEW_SHARED_USERS, CONF_IGNORE_PLEX_WEB_CLIENTS, CONF_MONITORED_USERS, + CONF_SERVER, DOMAIN, - PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .helpers import trigger_plex_update from .mock_classes import ( MockPlexAccount, MockPlexArtist, @@ -63,7 +64,7 @@ async def test_new_users_available(hass): server_id = mock_plex_server.machineIdentifier - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + trigger_plex_update(hass, mock_plex_server) await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -102,7 +103,7 @@ async def test_new_ignored_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + trigger_plex_update(hass, mock_plex_server) await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -125,109 +126,77 @@ async def test_new_ignored_users_available(hass, caplog): assert sensor.state == str(len(mock_plex_server.accounts)) -# class TestClockedPlex(ClockedTestCase): -# """Create clock-controlled tests.async_mock class.""" +async def test_network_error_during_refresh(hass, caplog): + """Test network failures during refreshes.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) -# async def setUp(self): -# """Initialize this test class.""" -# self.hass = await async_test_home_assistant(self.loop) + mock_plex_server = MockPlexServer() -# async def tearDown(self): -# """Clean up the HomeAssistant instance.""" -# await self.hass.async_stop() + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() -# async def test_mark_sessions_idle(self): -# """Test marking media_players as idle when sessions end.""" -# hass = self.hass + server_id = mock_plex_server.machineIdentifier + loaded_server = hass.data[DOMAIN][SERVERS][server_id] -# entry = MockConfigEntry( -# domain=DOMAIN, -# data=DEFAULT_DATA, -# options=DEFAULT_OPTIONS, -# unique_id=DEFAULT_DATA["server_id"], -# ) + trigger_plex_update(hass, mock_plex_server) + await hass.async_block_till_done() -# mock_plex_server = MockPlexServer(config_entry=entry) + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) -# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( -# "homeassistant.components.plex.PlexWebsocket.listen" -# ): -# entry.add_to_hass(hass) -# assert await hass.config_entries.async_setup(entry.entry_id) -# await hass.async_block_till_done() + with patch.object(mock_plex_server, "clients", side_effect=RequestException): + await loaded_server._async_update_platforms() + await hass.async_block_till_done() -# server_id = mock_plex_server.machineIdentifier + assert ( + f"Could not connect to Plex server: {DEFAULT_DATA[CONF_SERVER]}" in caplog.text + ) -# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) -# await hass.async_block_till_done() -# sensor = hass.states.get("sensor.plex_plex_server_1") -# assert sensor.state == str(len(mock_plex_server.accounts)) +async def test_mark_sessions_idle(hass): + """Test marking media_players as idle when sessions end.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) -# mock_plex_server.clear_clients() -# mock_plex_server.clear_sessions() + mock_plex_server = MockPlexServer() -# await self.advance(DEBOUNCE_TIMEOUT) -# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) -# await hass.async_block_till_done() + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() -# sensor = hass.states.get("sensor.plex_plex_server_1") -# assert sensor.state == "0" + server_id = mock_plex_server.machineIdentifier + loaded_server = hass.data[DOMAIN][SERVERS][server_id] -# async def test_debouncer(self): -# """Test debouncer behavior.""" -# hass = self.hass + trigger_plex_update(hass, mock_plex_server) + await hass.async_block_till_done() -# entry = MockConfigEntry( -# domain=DOMAIN, -# data=DEFAULT_DATA, -# options=DEFAULT_OPTIONS, -# unique_id=DEFAULT_DATA["server_id"], -# ) + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) -# mock_plex_server = MockPlexServer(config_entry=entry) + mock_plex_server.clear_clients() + mock_plex_server.clear_sessions() -# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( -# "homeassistant.components.plex.PlexWebsocket.listen" -# ): -# entry.add_to_hass(hass) -# assert await hass.config_entries.async_setup(entry.entry_id) -# await hass.async_block_till_done() + await loaded_server._async_update_platforms() + await hass.async_block_till_done() -# server_id = mock_plex_server.machineIdentifier - -# with patch.object(mock_plex_server, "clients", return_value=[]), patch.object( -# mock_plex_server, "sessions", return_value=[] -# ) as mock_update: -# # Called immediately -# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) -# await hass.async_block_till_done() -# assert mock_update.call_count == 1 - -# # Throttled -# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) -# await hass.async_block_till_done() -# assert mock_update.call_count == 1 - -# # Throttled -# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) -# await hass.async_block_till_done() -# assert mock_update.call_count == 1 - -# # Called from scheduler -# await self.advance(DEBOUNCE_TIMEOUT) -# await hass.async_block_till_done() -# assert mock_update.call_count == 2 - -# # Throttled -# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) -# await hass.async_block_till_done() -# assert mock_update.call_count == 2 - -# # Called from scheduler -# await self.advance(DEBOUNCE_TIMEOUT) -# await hass.async_block_till_done() -# assert mock_update.call_count == 3 + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == "0" async def test_ignore_plex_web_client(hass): @@ -252,9 +221,7 @@ async def test_ignore_plex_web_client(hass): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier - - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + trigger_plex_update(hass, mock_plex_server) await hass.async_block_till_done() sensor = hass.states.get("sensor.plex_plex_server_1") @@ -288,7 +255,7 @@ async def test_media_lookups(hass): loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Plex Key searches - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + trigger_plex_update(hass, mock_plex_server) await hass.async_block_till_done() media_player_id = hass.states.async_entity_ids("media_player")[0] From 0eaa6045c198ef15602cba8c713e38144f76f16f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Jun 2020 01:29:22 -0500 Subject: [PATCH 023/428] Ensure doorbird events can be filtered by entity_id (#37116) --- homeassistant/components/doorbird/__init__.py | 14 +++++++++++++- homeassistant/components/doorbird/logbook.py | 3 ++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index b4af58a740f..92ff25e7cf0 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DEVICES, CONF_HOST, CONF_NAME, @@ -25,7 +26,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import get_url from homeassistant.util import dt as dt_util, slugify -from .const import CONF_EVENTS, DOMAIN, DOOR_STATION, DOOR_STATION_INFO, PLATFORMS +from .const import ( + CONF_EVENTS, + DOMAIN, + DOOR_STATION, + DOOR_STATION_EVENT_ENTITY_IDS, + DOOR_STATION_INFO, + PLATFORMS, +) from .util import get_doorstation_by_token _LOGGER = logging.getLogger(__name__) @@ -362,6 +370,10 @@ class DoorBirdRequestView(HomeAssistantView): message = f"HTTP Favorites cleared for {device.slug}" return web.Response(status=HTTP_OK, text=message) + event_data[ATTR_ENTITY_ID] = hass.data[DOMAIN][ + DOOR_STATION_EVENT_ENTITY_IDS + ].get(event) + hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) return web.Response(status=HTTP_OK, text="OK") diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index ebe28cd350f..c7ed802b7ea 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -1,5 +1,6 @@ """Describe logbook events.""" +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from .const import DOMAIN, DOOR_STATION, DOOR_STATION_EVENT_ENTITY_IDS @@ -18,7 +19,7 @@ def async_describe_events(hass, async_describe_event): "name": "Doorbird", "message": f"Event {event.event_type} was fired.", "entity_id": hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS].get( - doorbird_event + doorbird_event, event.data.get(ATTR_ENTITY_ID) ), } From fe1a7f6d69130bc118e9c9731e52f279748af924 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 26 Jun 2020 15:15:54 +0200 Subject: [PATCH 024/428] Upgrade sqlalchemy to 1.3.18 (#37123) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 44396c6eccb..6a3933541f4 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.17"], + "requirements": ["sqlalchemy==1.3.18"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index a5caa1b1592..bdb1c0582b5 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,6 +2,6 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.3.17"], + "requirements": ["sqlalchemy==1.3.18"], "codeowners": ["@dgomes"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 824a1fe003a..0f3d1fd6d97 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ pytz>=2020.1 pyyaml==5.3.1 requests==2.24.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.17 +sqlalchemy==1.3.18 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.27.1 diff --git a/requirements_all.txt b/requirements_all.txt index 98a82483dc6..5d405d3025e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2022,7 +2022,7 @@ spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.17 +sqlalchemy==1.3.18 # homeassistant.components.starline starline==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb0dd8f49f2..5a2aaa5bebe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -866,7 +866,7 @@ spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.17 +sqlalchemy==1.3.18 # homeassistant.components.starline starline==0.1.3 From 76db2b39b0eae0574604a7305a68afa7e56c2f76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Jun 2020 09:12:50 -0500 Subject: [PATCH 025/428] Move logbook continuous domain filtering to sql (#37115) * Move logbook continuous domain filtering to sql sensors tend to generate a significant amount of states that are filtered out by logbook. In testing 75% of states can be filtered away in sql to avoid the sqlalchemy ORM overhead of creating objects that will be discarded. * remove un-needed nesting --- homeassistant/components/logbook/__init__.py | 19 ++++--- tests/components/logbook/test_init.py | 52 ++++++++++++++------ 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 28d6c7fcd48..ad14ce71733 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -5,6 +5,7 @@ import json import logging import time +import sqlalchemy from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import aliased import voluptuous as vol @@ -28,7 +29,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, CONF_EXCLUDE, CONF_INCLUDE, EVENT_HOMEASSISTANT_START, @@ -66,6 +66,8 @@ DOMAIN = "logbook" GROUP_BY_MINUTES = 15 EMPTY_JSON_OBJECT = "{}" +UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' + CONFIG_SCHEMA = vol.Schema( {DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA ) @@ -414,6 +416,15 @@ def _get_events(hass, config, start_day, end_day, entity_id=None): & (States.state != old_state.state) ) ) + # + # Prefilter out continuous domains that have + # ATTR_UNIT_OF_MEASUREMENT as its much faster in sql. + # + .filter( + (Events.event_type != EVENT_STATE_CHANGED) + | sqlalchemy.not_(States.domain.in_(CONTINUOUS_DOMAINS)) + | sqlalchemy.not_(States.attributes.contains(UNIT_OF_MEASUREMENT_JSON)) + ) .filter( Events.event_type.in_(ALL_EVENT_TYPES + list(hass.data.get(DOMAIN, {}))) ) @@ -449,12 +460,6 @@ def _keep_event(hass, event, entities_filter, entity_attr_cache): # Do not report on entity removal if not event.has_old_and_new_state: return False - - if event.domain in CONTINUOUS_DOMAINS and entity_attr_cache.get( - entity_id, ATTR_UNIT_OF_MEASUREMENT, event - ): - # Don't show continuous sensor value changes in the logbook - return False elif event.event_type in HOMEASSISTANT_EVENTS: entity_id = f"{HA_DOMAIN}." elif event.event_type in hass.data[DOMAIN] and ATTR_ENTITY_ID not in event.data: diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 03ef09b438d..464bda088d6 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -149,22 +149,6 @@ class TestComponentLogbook(unittest.TestCase): entries[1], pointC, "bla", domain="sensor", entity_id=entity_id ) - def test_filter_continuous_sensor_values(self): - """Test remove continuous sensor events from logbook.""" - entity_id = "sensor.bla" - pointA = dt_util.utcnow() - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - attributes = {"unit_of_measurement": "foo"} - eventA = self.create_state_changed_event(pointA, entity_id, 10, attributes) - - entities_filter = convert_include_exclude_filter( - logbook.CONFIG_SCHEMA({logbook.DOMAIN: {}})[logbook.DOMAIN] - ) - assert ( - logbook._keep_event(self.hass, eventA, entities_filter, entity_attr_cache) - is False - ) - def test_exclude_new_entities(self): """Test if events are excluded on first update.""" entity_id = "sensor.bla" @@ -1806,6 +1790,42 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client): assert json_dict[0]["entity_id"] == entity_id_second +async def test_filter_continuous_sensor_values(hass, hass_client): + """Test remove continuous sensor events from logbook.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + entity_id_test = "switch.test" + hass.states.async_set(entity_id_test, STATE_OFF) + hass.states.async_set(entity_id_test, STATE_ON) + entity_id_second = "sensor.bla" + hass.states.async_set(entity_id_second, STATE_OFF, {"unit_of_measurement": "foo"}) + hass.states.async_set(entity_id_second, STATE_ON, {"unit_of_measurement": "foo"}) + entity_id_third = "light.bla" + hass.states.async_set(entity_id_third, STATE_OFF, {"unit_of_measurement": "foo"}) + hass.states.async_set(entity_id_third, STATE_ON, {"unit_of_measurement": "foo"}) + + await hass.async_add_job(partial(trigger_db_commit, hass)) + await hass.async_block_till_done() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_client() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries without filters + response = await client.get(f"/api/logbook/{start_date.isoformat()}") + assert response.status == 200 + response_json = await response.json() + + assert len(response_json) == 2 + assert response_json[0]["entity_id"] == entity_id_test + assert response_json[1]["entity_id"] == entity_id_third + + class MockLazyEventPartialState(ha.Event): """Minimal mock of a Lazy event.""" From 3a2d4ac7fa06809d982d1c81f3fcc36bc879db63 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 26 Jun 2020 10:19:38 -0600 Subject: [PATCH 026/428] Add optimistic Guardian switch updating (#37141) --- homeassistant/components/guardian/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 9917482b5b6..39945f4da4e 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -70,6 +70,7 @@ class GuardianSwitch(GuardianEntity, SwitchEntity): return self._is_on = False + self.async_write_ha_state() async def async_turn_on(self, **kwargs) -> None: """Turn the valve on (open).""" @@ -81,3 +82,4 @@ class GuardianSwitch(GuardianEntity, SwitchEntity): return self._is_on = True + self.async_write_ha_state() From 39a5f6891427c9ada77340f143e051a31bcbf0e3 Mon Sep 17 00:00:00 2001 From: Kdemontf <49533626+Kdemontf@users.noreply.github.com> Date: Fri, 26 Jun 2020 12:29:38 -0400 Subject: [PATCH 027/428] Update remote_rpi_gpio switch parent (#37136) * Update switch.py Update to rename SwitchDevice to SwitchEntity, if appropriate. * Update switch.py * Update switch.py --- homeassistant/components/remote_rpi_gpio/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 42ce258ef98..48e072a5d6b 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -43,8 +43,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class RemoteRPiGPIOSwitch(SwitchDevice): - """Representation of a Remtoe Raspberry Pi GPIO.""" +class RemoteRPiGPIOSwitch(SwitchEntity): + """Representation of a Remote Raspberry Pi GPIO.""" def __init__(self, name, led): """Initialize the pin.""" From a4501b93c4c75b32387105b753a6be5c5f2ceaa1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Jun 2020 11:45:40 -0500 Subject: [PATCH 028/428] Fix repack when using pymysql (#37142) --- homeassistant/components/recorder/purge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 78d92b8b65e..6314a410c8b 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -46,7 +46,7 @@ def purge_old_data(instance, purge_days, repack): _LOGGER.debug("Vacuuming SQL DB to free space") instance.engine.execute("VACUUM") # Optimize mysql / mariadb tables to free up space on disk - elif instance.engine.driver == "mysqldb": + elif instance.engine.driver in ("mysqldb", "pymysql"): _LOGGER.debug("Optimizing SQL DB to free space") instance.engine.execute("OPTIMIZE TABLE states, events, recorder_runs") From 7d74b74570abb4c51c84a1eb9092d2f0a48d7550 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 26 Jun 2020 19:27:45 +0200 Subject: [PATCH 029/428] Fix recorder purging by batch processing purges (#37140) --- homeassistant/components/recorder/__init__.py | 6 ++- homeassistant/components/recorder/models.py | 4 +- homeassistant/components/recorder/purge.py | 50 +++++++++++++++---- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_purge.py | 21 ++++++-- 5 files changed, 66 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 52dabad1faf..aadc8e61fa1 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -335,7 +335,7 @@ class Recorder(threading.Thread): self.event_session = self.get_session() # Use a session for the event read loop # with a commit every time the event time - # has changed. This reduces the disk io. + # has changed. This reduces the disk io. while True: event = self.queue.get() if event is None: @@ -344,7 +344,9 @@ class Recorder(threading.Thread): self.queue.task_done() return if isinstance(event, PurgeTask): - purge.purge_old_data(self, event.keep_days, event.repack) + # Schedule a new purge task if this one didn't finish + if not purge.purge_old_data(self, event.keep_days, event.repack): + self.queue.put(PurgeTask(event.keep_days, event.repack)) self.queue.task_done() continue if event.event_type == EVENT_TIME_CHANGED: diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 03c81726310..0566faf1c4d 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -64,7 +64,7 @@ class Events(Base): # type: ignore context_parent_id=event.context.parent_id, ) - def to_native(self): + def to_native(self, validate_entity_id=True): """Convert to a natve HA Event.""" context = Context( id=self.context_id, @@ -183,7 +183,7 @@ class RecorderRuns(Base): # type: ignore return [row[0] for row in query] - def to_native(self): + def to_native(self, validate_entity_id=True): """Return self, native format is this model.""" return self diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 6314a410c8b..8b0b71e24ae 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -7,32 +7,62 @@ from sqlalchemy.exc import SQLAlchemyError import homeassistant.util.dt as dt_util from .models import Events, RecorderRuns, States -from .util import session_scope +from .util import execute, session_scope _LOGGER = logging.getLogger(__name__) -def purge_old_data(instance, purge_days, repack): - """Purge events and states older than purge_days ago.""" +def purge_old_data(instance, purge_days: int, repack: bool) -> bool: + """Purge events and states older than purge_days ago. + + Cleans up an timeframe of an hour, based on the oldest record. + """ purge_before = dt_util.utcnow() - timedelta(days=purge_days) _LOGGER.debug("Purging events before %s", purge_before) try: with session_scope(session=instance.get_session()) as session: - deleted_rows = ( + query = session.query(States).order_by(States.last_updated.asc()).limit(1) + states = execute(query, to_native=True, validate_entity_ids=False) + + states_purge_before = purge_before + if states: + states_purge_before = min( + purge_before, states[0].last_updated + timedelta(hours=1) + ) + + deleted_rows_states = ( session.query(States) - .filter(States.last_updated < purge_before) + .filter(States.last_updated < states_purge_before) .delete(synchronize_session=False) ) - _LOGGER.debug("Deleted %s states", deleted_rows) + _LOGGER.debug("Deleted %s states", deleted_rows_states) - deleted_rows = ( + query = session.query(Events).order_by(Events.time_fired.asc()).limit(1) + events = execute(query, to_native=True) + + events_purge_before = purge_before + if events: + events_purge_before = min( + purge_before, events[0].time_fired + timedelta(hours=1) + ) + + deleted_rows_events = ( session.query(Events) - .filter(Events.time_fired < purge_before) + .filter(Events.time_fired < events_purge_before) .delete(synchronize_session=False) ) - _LOGGER.debug("Deleted %s events", deleted_rows) + _LOGGER.debug("Deleted %s events", deleted_rows_events) + # If states or events purging isn't processing the purge_before yet, + # return false, as we are not done yet. + if (states_purge_before and states_purge_before != purge_before) or ( + events_purge_before and events_purge_before != purge_before + ): + _LOGGER.debug("Purging hasn't fully completed yet.") + return False + + # Recorder runs is small, no need to batch run it deleted_rows = ( session.query(RecorderRuns) .filter(RecorderRuns.start < purge_before) @@ -52,3 +82,5 @@ def purge_old_data(instance, purge_days, repack): except SQLAlchemyError as err: _LOGGER.warning("Error purging history: %s.", err) + + return True diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index d4dfa0ecc1e..843609cf308 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -317,7 +317,7 @@ def test_auto_purge(hass_recorder): test_time = tz.localize(datetime(2020, 1, 1, 4, 12, 0)) with patch( - "homeassistant.components.recorder.purge.purge_old_data" + "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data: for delta in (-1, 0, 1): hass.bus.fire( diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 05a184a8608..afcb1b2818f 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -130,9 +130,16 @@ class TestRecorderPurge(unittest.TestCase): assert states.count() == 6 # run purge_old_data() - purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False) + finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False) + assert not finished + assert states.count() == 4 - # we should only have 2 states left after purging + finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False) + assert not finished + assert states.count() == 2 + + finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False) + assert finished assert states.count() == 2 def test_purge_old_events(self): @@ -144,9 +151,17 @@ class TestRecorderPurge(unittest.TestCase): assert events.count() == 6 # run purge_old_data() - purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False) + finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False) + assert not finished + assert events.count() == 4 + + finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False) + assert not finished + assert events.count() == 2 # we should only have 2 events left + finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False) + assert finished assert events.count() == 2 def test_purge_method(self): From fe5bf96e5d8e333cf13b07c2c1aa720c766b8943 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 26 Jun 2020 13:30:44 -0400 Subject: [PATCH 030/428] Catch additional exception for Plex account login failures (#37143) --- homeassistant/components/plex/__init__.py | 2 +- homeassistant/components/plex/server.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index c83dfe13347..01f80ed0d2b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -164,7 +164,7 @@ async def async_setup_entry(hass, entry): def get_plex_account(plex_server): try: return plex_server.account - except plexapi.exceptions.Unauthorized: + except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): return None plex_account = await hass.async_add_executor_job(get_plex_account, plex_server) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 1a32e08bcf6..4a09cf7d339 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -4,7 +4,7 @@ import ssl import time from urllib.parse import urlparse -from plexapi.exceptions import NotFound, Unauthorized +from plexapi.exceptions import BadRequest, NotFound, Unauthorized import plexapi.myplex import plexapi.playqueue import plexapi.server @@ -98,7 +98,7 @@ class PlexServer: if not self._plex_account and self._use_plex_tv: try: self._plex_account = plexapi.myplex.MyPlexAccount(token=self._token) - except Unauthorized: + except (BadRequest, Unauthorized): self._use_plex_tv = False _LOGGER.error("Not authorized to access plex.tv with provided token") raise From 680f8f8d5a6bcedb8fa36d8f909bd0964e5f76ea Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Fri, 26 Jun 2020 19:46:14 +0200 Subject: [PATCH 031/428] Improve Smappee integration (#37087) --- homeassistant/components/smappee/__init__.py | 16 +++++++++++++--- homeassistant/components/smappee/api.py | 16 +++++++++++++++- .../components/smappee/binary_sensor.py | 6 ------ .../components/smappee/config_flow.py | 7 ------- homeassistant/components/smappee/const.py | 12 ++++++++++-- homeassistant/components/smappee/strings.json | 1 - tests/components/smappee/test_config_flow.py | 18 +++--------------- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index c0c6c41ad54..381678f0e86 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -5,7 +5,7 @@ from pysmappee import Smappee import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.util import Throttle @@ -40,6 +40,15 @@ async def async_setup(hass: HomeAssistant, config: dict): if DOMAIN not in config: return True + # decide platform + platform = "PRODUCTION" + if config[DOMAIN][CONF_CLIENT_ID] == "homeassistant_f2": + platform = "ACCEPTANCE" + elif config[DOMAIN][CONF_CLIENT_ID] == "homeassistant_f3": + platform = "DEVELOPMENT" + + hass.data[DOMAIN][CONF_PLATFORM] = platform + config_flow.SmappeeFlowHandler.async_register_implementation( hass, config_entry_oauth2_flow.LocalOAuth2Implementation( @@ -47,8 +56,8 @@ async def async_setup(hass: HomeAssistant, config: dict): DOMAIN, config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET], - AUTHORIZE_URL, - TOKEN_URL, + AUTHORIZE_URL[platform], + TOKEN_URL[platform], ), ) @@ -89,6 +98,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if unload_ok: hass.data[DOMAIN].pop(BASE, None) + hass.data[DOMAIN].pop(CONF_PLATFORM, None) return unload_ok diff --git a/homeassistant/components/smappee/api.py b/homeassistant/components/smappee/api.py index 703dd581d31..8de1d4cfec5 100644 --- a/homeassistant/components/smappee/api.py +++ b/homeassistant/components/smappee/api.py @@ -4,8 +4,11 @@ from asyncio import run_coroutine_threadsafe from pysmappee import api from homeassistant import config_entries, core +from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import config_entry_oauth2_flow +from .const import DOMAIN + class ConfigEntrySmappeeApi(api.SmappeeApi): """Provide Smappee authentication tied to an OAuth2 based config entry.""" @@ -22,7 +25,18 @@ class ConfigEntrySmappeeApi(api.SmappeeApi): self.session = config_entry_oauth2_flow.OAuth2Session( hass, config_entry, implementation ) - super().__init__(None, None, token=self.session.token) + + platform_to_farm = { + "PRODUCTION": 1, + "ACCEPTANCE": 2, + "DEVELOPMENT": 3, + } + super().__init__( + None, + None, + token=self.session.token, + farm=platform_to_farm[hass.data[DOMAIN][CONF_PLATFORM]], + ) def refresh_tokens(self) -> dict: """Refresh and return new Smappee tokens using Home Assistant OAuth2 session.""" diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py index 7de6766bcb3..9b55f358ef3 100644 --- a/homeassistant/components/smappee/binary_sensor.py +++ b/homeassistant/components/smappee/binary_sensor.py @@ -139,12 +139,6 @@ class SmappeeAppliance(BinarySensorEntity): } return icon_mapping.get(self._appliance_type) - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - # Only lights can be mapped onto the generic list of binary sensors - return "light" if self._appliance_type == "Lights" else "power" - @property def unique_id(self,): """Return the unique ID for this binary sensor.""" diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index 48aca5449ad..e07c3b65e37 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -21,10 +21,3 @@ class SmappeeFlowHandler( def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) - - async def async_step_user(self, user_input=None): - """Handle a flow start.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="single_instance_allowed") - - return await super().async_step_user(user_input) diff --git a/homeassistant/components/smappee/const.py b/homeassistant/components/smappee/const.py index 05998ac6716..4bc370e9c09 100644 --- a/homeassistant/components/smappee/const.py +++ b/homeassistant/components/smappee/const.py @@ -11,5 +11,13 @@ SMAPPEE_PLATFORMS = ["binary_sensor", "sensor", "switch"] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -AUTHORIZE_URL = "https://app1pub.smappee.net/dev/v1/oauth2/authorize" -TOKEN_URL = "https://app1pub.smappee.net/dev/v3/oauth2/token" +AUTHORIZE_URL = { + "PRODUCTION": "https://app1pub.smappee.net/dev/v1/oauth2/authorize", + "ACCEPTANCE": "https://farm2pub.smappee.net/dev/v1/oauth2/authorize", + "DEVELOPMENT": "https://farm3pub.smappee.net/dev/v1/oauth2/authorize", +} +TOKEN_URL = { + "PRODUCTION": "https://app1pub.smappee.net/dev/v3/oauth2/token", + "ACCEPTANCE": "https://farm2pub.smappee.net/dev/v3/oauth2/token", + "DEVELOPMENT": "https://farm3pub.smappee.net/dev/v3/oauth2/token", +} diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json index d89d15a92c6..6b86bd042ac 100644 --- a/homeassistant/components/smappee/strings.json +++ b/homeassistant/components/smappee/strings.json @@ -7,7 +7,6 @@ }, "abort": { "authorize_url_timeout": "Timeout generating authorize url.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "missing_configuration": "The component is not configured. Please follow the documentation." } } diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index e37733a1385..265cfde69bb 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -1,27 +1,15 @@ """Test the Smappee config flow.""" -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.smappee.const import AUTHORIZE_URL, DOMAIN, TOKEN_URL from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow from tests.async_mock import patch -from tests.common import MockConfigEntry CLIENT_ID = "1234" CLIENT_SECRET = "5678" -async def test_abort_if_existing_entry(hass): - """Check flow abort when an entry already exist.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" - - async def test_full_flow(hass, aiohttp_client, aioclient_mock): """Check full flow.""" assert await setup.async_setup_component( @@ -39,7 +27,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) assert result["url"] == ( - f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + f"{AUTHORIZE_URL['PRODUCTION']}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" ) @@ -50,7 +38,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( - TOKEN_URL, + TOKEN_URL["PRODUCTION"], json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", From 8a9b19f3270ce0a636715c9e2b9a6514e2f2897b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Jun 2020 13:44:28 -0700 Subject: [PATCH 032/428] Updated frontend to 20200626.1 (#37150) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 546ca70cff3..6fc6bba73ff 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200626.0"], + "requirements": [ + "home-assistant-frontend==20200626.1" + ], "dependencies": [ "api", "auth", @@ -15,6 +17,8 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"], + "codeowners": [ + "@home-assistant/frontend" + ], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0f3d1fd6d97..9bce53ba733 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.34.7 -home-assistant-frontend==20200626.0 +home-assistant-frontend==20200626.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 netdisco==2.7.1 diff --git a/requirements_all.txt b/requirements_all.txt index 5d405d3025e..85e5c215209 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200626.0 +home-assistant-frontend==20200626.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a2aaa5bebe..dc388191b45 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200626.0 +home-assistant-frontend==20200626.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 50dd6b69cddd410092c22fd75547458e6dee7853 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Jun 2020 14:25:50 -0700 Subject: [PATCH 033/428] Fix OwnTracks race condition (#37152) --- .../components/owntracks/device_tracker.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index b1204082887..d4a5399a0ff 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -24,6 +24,19 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up OwnTracks based off an entry.""" + # Restore previously loaded devices + dev_reg = await device_registry.async_get_registry(hass) + dev_ids = { + identifier[1] + for device in dev_reg.devices.values() + for identifier in device.identifiers + if identifier[0] == OT_DOMAIN + } + + entities = [] + for dev_id in dev_ids: + entity = hass.data[OT_DOMAIN]["devices"][dev_id] = OwnTracksEntity(dev_id) + entities.append(entity) @callback def _receive_data(dev_id, **data): @@ -39,24 +52,8 @@ async def async_setup_entry(hass, entry, async_add_entities): hass.data[OT_DOMAIN]["context"].set_async_see(_receive_data) - # Restore previously loaded devices - dev_reg = await device_registry.async_get_registry(hass) - dev_ids = { - identifier[1] - for device in dev_reg.devices.values() - for identifier in device.identifiers - if identifier[0] == OT_DOMAIN - } - - if not dev_ids: - return True - - entities = [] - for dev_id in dev_ids: - entity = hass.data[OT_DOMAIN]["devices"][dev_id] = OwnTracksEntity(dev_id) - entities.append(entity) - - async_add_entities(entities) + if entities: + async_add_entities(entities) return True From 4e10ca321480900376e63d2e38176cf6c920a2c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Jun 2020 14:26:05 -0700 Subject: [PATCH 034/428] Fix speedtest blowing up (#37151) --- homeassistant/components/speedtestdotnet/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 311a1a0d0d3..1d8f3cf189b 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -94,7 +94,7 @@ class SpeedTestOptionsFlowHandler(config_entries.OptionsFlow): for (key, value) in self._servers.items() if value.get("id") == self.config_entry.options[CONF_SERVER_ID] ] - server_name = server[0] + server_name = server[0] if server else "" options = { vol.Optional( From d454f855726c15cffdd772b2ba71975d0a5946af Mon Sep 17 00:00:00 2001 From: mdegat01 Date: Fri, 26 Jun 2020 18:01:32 -0400 Subject: [PATCH 035/428] Add support for glob matching in InfluxDB filters (#37069) * added support for glob filtering to influx * removed print and using dataclass instead of namedtuple --- homeassistant/components/influxdb/__init__.py | 46 +- tests/components/influxdb/test_init.py | 436 ++++++++++-------- 2 files changed, 245 insertions(+), 237 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 9823d57e200..94a68c25504 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -12,15 +12,12 @@ from influxdb_client import InfluxDBClient as InfluxDBClientV2 from influxdb_client.client.write_api import ASYNCHRONOUS, SYNCHRONOUS from influxdb_client.rest import ApiException import requests.exceptions +import urllib3.exceptions import voluptuous as vol from homeassistant.const import ( CONF_API_VERSION, - CONF_DOMAINS, - CONF_ENTITIES, - CONF_EXCLUDE, CONF_HOST, - CONF_INCLUDE, CONF_PASSWORD, CONF_PATH, CONF_PORT, @@ -37,6 +34,10 @@ from homeassistant.const import ( from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues +from homeassistant.helpers.entityfilter import ( + INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, + convert_include_exclude_filter, +) _LOGGER = logging.getLogger(__name__) @@ -141,24 +142,8 @@ COMPONENT_CONFIG_SCHEMA_CONNECTION = { _CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string}) -_CONFIG_SCHEMA = vol.Schema( +_CONFIG_SCHEMA = INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( { - vol.Optional(CONF_EXCLUDE, default={}): vol.Schema( - { - vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ), - vol.Optional(CONF_INCLUDE, default={}): vol.Schema( - { - vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ), vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int, vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string, vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, @@ -253,12 +238,7 @@ def setup(hass, config): if CONF_SSL in conf: kwargs["ssl"] = conf[CONF_SSL] - include = conf.get(CONF_INCLUDE, {}) - exclude = conf.get(CONF_EXCLUDE, {}) - whitelist_e = set(include.get(CONF_ENTITIES, [])) - whitelist_d = set(include.get(CONF_DOMAINS, [])) - blacklist_e = set(exclude.get(CONF_ENTITIES, [])) - blacklist_d = set(exclude.get(CONF_DOMAINS, [])) + entity_filter = convert_include_exclude_filter(conf) tags = conf.get(CONF_TAGS) tags_attributes = conf.get(CONF_TAGS_ATTRIBUTES) default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT) @@ -285,7 +265,7 @@ def setup(hass, config): ) event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) return True - except ApiException as exc: + except (ApiException, urllib3.exceptions.HTTPError) as exc: _LOGGER.error( "Bucket is not accessible due to '%s', please " "check your entries in the configuration file (url, org, " @@ -303,19 +283,11 @@ def setup(hass, config): if ( state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) - or state.entity_id in blacklist_e - or state.domain in blacklist_d + or not entity_filter(state.entity_id) ): return try: - if ( - (whitelist_e or whitelist_d) - and state.entity_id not in whitelist_e - and state.domain not in whitelist_d - ): - return - _include_state = _include_value = False _state_as_value = float(state.state) diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index f9514f7ebff..29247bec9c8 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,4 +1,5 @@ """The tests for the InfluxDB component.""" +from dataclasses import dataclass import datetime import pytest @@ -11,6 +12,7 @@ from homeassistant.const import ( STATE_STANDBY, UNIT_PERCENTAGE, ) +from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component from tests.async_mock import MagicMock, Mock, call, patch @@ -23,6 +25,14 @@ BASE_V2_CONFIG = { } +@dataclass +class FilterTest: + """Class for capturing a filter test.""" + + id: str + should_pass: bool + + @pytest.fixture(autouse=True) def mock_batch_timeout(hass, monkeypatch): """Mock the event bus listener and the batch timeout for tests.""" @@ -421,43 +431,22 @@ async def test_event_listener_states( write_api.reset_mock() -@pytest.mark.parametrize( - "mock_client, config_ext, get_write_api, get_mock_call", - [ - ( - influxdb.DEFAULT_API_VERSION, - BASE_V1_CONFIG, - _get_write_api_mock_v1, - influxdb.DEFAULT_API_VERSION, - ), - ( - influxdb.API_VERSION_2, - BASE_V2_CONFIG, - _get_write_api_mock_v2, - influxdb.API_VERSION_2, - ), - ], - indirect=["mock_client", "get_mock_call"], -) -async def test_event_listener_blacklist( - hass, mock_client, config_ext, get_write_api, get_mock_call -): - """Test the event listener against a blacklist.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) - - for entity_id in ("ok", "blacklisted"): +def execute_filter_test(hass, tests, handler_method, write_api, get_mock_call): + """Execute all tests for a given filtering test.""" + for test in tests: + domain, entity_id = split_entity_id(test.id) state = MagicMock( state=1, - domain="fake", - entity_id=f"fake.{entity_id}", + domain=domain, + entity_id=test.id, object_id=entity_id, attributes={}, ) event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { - "measurement": f"fake.{entity_id}", - "tags": {"domain": "fake", "entity_id": entity_id}, + "measurement": test.id, + "tags": {"domain": domain, "entity_id": entity_id}, "time": 12345, "fields": {"value": 1}, } @@ -465,9 +454,8 @@ async def test_event_listener_blacklist( handler_method(event) hass.data[influxdb.DOMAIN].block_till_done() - write_api = get_write_api(mock_client) - if entity_id == "ok": - assert write_api.call_count == 1 + if test.should_pass: + write_api.assert_called_once() assert write_api.call_args == get_mock_call(body) else: assert not write_api.called @@ -492,94 +480,20 @@ async def test_event_listener_blacklist( ], indirect=["mock_client", "get_mock_call"], ) -async def test_event_listener_blacklist_domain( +async def test_event_listener_denylist( hass, mock_client, config_ext, get_write_api, get_mock_call ): - """Test the event listener against a domain blacklist.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) - - for domain in ("ok", "another_fake"): - state = MagicMock( - state=1, - domain=domain, - entity_id=f"{domain}.something", - object_id="something", - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) - body = [ - { - "measurement": f"{domain}.something", - "tags": {"domain": domain, "entity_id": "something"}, - "time": 12345, - "fields": {"value": 1}, - } - ] - handler_method(event) - hass.data[influxdb.DOMAIN].block_till_done() - - write_api = get_write_api(mock_client) - if domain == "ok": - assert write_api.call_count == 1 - assert write_api.call_args == get_mock_call(body) - else: - assert not write_api.called - write_api.reset_mock() - - -@pytest.mark.parametrize( - "mock_client, config_ext, get_write_api, get_mock_call", - [ - ( - influxdb.DEFAULT_API_VERSION, - BASE_V1_CONFIG, - _get_write_api_mock_v1, - influxdb.DEFAULT_API_VERSION, - ), - ( - influxdb.API_VERSION_2, - BASE_V2_CONFIG, - _get_write_api_mock_v2, - influxdb.API_VERSION_2, - ), - ], - indirect=["mock_client", "get_mock_call"], -) -async def test_event_listener_whitelist( - hass, mock_client, config_ext, get_write_api, get_mock_call -): - """Test the event listener against a whitelist.""" - config = {"include": {"entities": ["fake.included"]}} + """Test the event listener against a denylist.""" + config = {"exclude": {"entities": ["fake.denylisted"]}, "include": {}} config.update(config_ext) handler_method = await _setup(hass, mock_client, config, get_write_api) + write_api = get_write_api(mock_client) - for entity_id in ("included", "default"): - state = MagicMock( - state=1, - domain="fake", - entity_id=f"fake.{entity_id}", - object_id=entity_id, - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) - body = [ - { - "measurement": f"fake.{entity_id}", - "tags": {"domain": "fake", "entity_id": entity_id}, - "time": 12345, - "fields": {"value": 1}, - } - ] - handler_method(event) - hass.data[influxdb.DOMAIN].block_till_done() - - write_api = get_write_api(mock_client) - if entity_id == "included": - assert write_api.call_count == 1 - assert write_api.call_args == get_mock_call(body) - else: - assert not write_api.called - write_api.reset_mock() + tests = [ + FilterTest("fake.ok", True), + FilterTest("fake.denylisted", False), + ] + execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) @pytest.mark.parametrize( @@ -600,41 +514,20 @@ async def test_event_listener_whitelist( ], indirect=["mock_client", "get_mock_call"], ) -async def test_event_listener_whitelist_domain( +async def test_event_listener_denylist_domain( hass, mock_client, config_ext, get_write_api, get_mock_call ): - """Test the event listener against a domain whitelist.""" - config = {"include": {"domains": ["fake"]}} + """Test the event listener against a domain denylist.""" + config = {"exclude": {"domains": ["another_fake"]}, "include": {}} config.update(config_ext) handler_method = await _setup(hass, mock_client, config, get_write_api) + write_api = get_write_api(mock_client) - for domain in ("fake", "another_fake"): - state = MagicMock( - state=1, - domain=domain, - entity_id=f"{domain}.something", - object_id="something", - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) - body = [ - { - "measurement": f"{domain}.something", - "tags": {"domain": domain, "entity_id": "something"}, - "time": 12345, - "fields": {"value": 1}, - } - ] - handler_method(event) - hass.data[influxdb.DOMAIN].block_till_done() - - write_api = get_write_api(mock_client) - if domain == "fake": - assert write_api.call_count == 1 - assert write_api.call_args == get_mock_call(body) - else: - assert not write_api.called - write_api.reset_mock() + tests = [ + FilterTest("fake.ok", True), + FilterTest("another_fake.denylisted", False), + ] + execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) @pytest.mark.parametrize( @@ -655,69 +548,212 @@ async def test_event_listener_whitelist_domain( ], indirect=["mock_client", "get_mock_call"], ) -async def test_event_listener_whitelist_domain_and_entities( +async def test_event_listener_denylist_glob( hass, mock_client, config_ext, get_write_api, get_mock_call ): - """Test the event listener against a domain and entity whitelist.""" - config = {"include": {"domains": ["fake"], "entities": ["other.one"]}} + """Test the event listener against a glob denylist.""" + config = {"exclude": {"entity_globs": ["*.excluded_*"]}, "include": {}} config.update(config_ext) handler_method = await _setup(hass, mock_client, config, get_write_api) + write_api = get_write_api(mock_client) - for domain in ("fake", "another_fake"): - state = MagicMock( - state=1, - domain=domain, - entity_id=f"{domain}.something", - object_id="something", - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) - body = [ - { - "measurement": f"{domain}.something", - "tags": {"domain": domain, "entity_id": "something"}, - "time": 12345, - "fields": {"value": 1}, - } - ] - handler_method(event) - hass.data[influxdb.DOMAIN].block_till_done() + tests = [ + FilterTest("fake.ok", True), + FilterTest("fake.excluded_entity", False), + ] + execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) - write_api = get_write_api(mock_client) - if domain == "fake": - assert write_api.call_count == 1 - assert write_api.call_args == get_mock_call(body) - else: - assert not write_api.called - write_api.reset_mock() - for entity_id in ("one", "two"): - state = MagicMock( - state=1, - domain="other", - entity_id=f"other.{entity_id}", - object_id=entity_id, - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) - body = [ - { - "measurement": f"other.{entity_id}", - "tags": {"domain": "other", "entity_id": entity_id}, - "time": 12345, - "fields": {"value": 1}, - } - ] - handler_method(event) - hass.data[influxdb.DOMAIN].block_till_done() +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_event_listener_allowlist( + hass, mock_client, config_ext, get_write_api, get_mock_call +): + """Test the event listener against an allowlist.""" + config = {"include": {"entities": ["fake.included"]}, "exclude": {}} + config.update(config_ext) + handler_method = await _setup(hass, mock_client, config, get_write_api) + write_api = get_write_api(mock_client) - write_api = get_write_api(mock_client) - if entity_id == "one": - assert write_api.call_count == 1 - assert write_api.call_args == get_mock_call(body) - else: - assert not write_api.called - write_api.reset_mock() + tests = [ + FilterTest("fake.included", True), + FilterTest("fake.excluded", False), + ] + execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + + +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_event_listener_allowlist_domain( + hass, mock_client, config_ext, get_write_api, get_mock_call +): + """Test the event listener against a domain allowlist.""" + config = {"include": {"domains": ["fake"]}, "exclude": {}} + config.update(config_ext) + handler_method = await _setup(hass, mock_client, config, get_write_api) + write_api = get_write_api(mock_client) + + tests = [ + FilterTest("fake.ok", True), + FilterTest("another_fake.excluded", False), + ] + execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + + +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_event_listener_allowlist_glob( + hass, mock_client, config_ext, get_write_api, get_mock_call +): + """Test the event listener against a glob allowlist.""" + config = {"include": {"entity_globs": ["*.included_*"]}, "exclude": {}} + config.update(config_ext) + handler_method = await _setup(hass, mock_client, config, get_write_api) + write_api = get_write_api(mock_client) + + tests = [ + FilterTest("fake.included_entity", True), + FilterTest("fake.denied", False), + ] + execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + + +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_event_listener_filtered_allowlist( + hass, mock_client, config_ext, get_write_api, get_mock_call +): + """Test the event listener against an allowlist filtered by denylist.""" + config = { + "include": { + "domains": ["fake"], + "entities": ["another_fake.included"], + "entity_globs": "*.included_*", + }, + "exclude": { + "entities": ["fake.excluded"], + "domains": ["another_fake"], + "entity_globs": "*.excluded_*", + }, + } + config.update(config_ext) + handler_method = await _setup(hass, mock_client, config, get_write_api) + write_api = get_write_api(mock_client) + + tests = [ + FilterTest("fake.ok", True), + FilterTest("another_fake.included", True), + FilterTest("test.included_entity", True), + FilterTest("fake.excluded", False), + FilterTest("another_fake.denied", False), + FilterTest("fake.excluded_entity", False), + FilterTest("another_fake.included_entity", False), + ] + execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + + +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_event_listener_filtered_denylist( + hass, mock_client, config_ext, get_write_api, get_mock_call +): + """Test the event listener against a domain/glob denylist with an entity id allowlist.""" + config = { + "include": {"entities": ["another_fake.included", "fake.excluded_pass"]}, + "exclude": {"domains": ["another_fake"], "entity_globs": "*.excluded_*"}, + } + config.update(config_ext) + handler_method = await _setup(hass, mock_client, config, get_write_api) + write_api = get_write_api(mock_client) + + tests = [ + FilterTest("fake.ok", True), + FilterTest("another_fake.included", True), + FilterTest("fake.excluded_pass", True), + FilterTest("another_fake.denied", False), + FilterTest("fake.excluded_entity", False), + ] + execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) @pytest.mark.parametrize( From e48bcd2070c84a1cac59120786335eba4037f206 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 27 Jun 2020 00:05:54 +0000 Subject: [PATCH 036/428] [ci skip] Translation update --- .../components/adguard/translations/cs.json | 8 +++ .../components/agent_dvr/translations/cs.json | 12 ++++ .../components/airly/translations/cs.json | 11 ++++ .../components/airvisual/translations/cs.json | 16 +++++ .../ambient_station/translations/cs.json | 11 ++++ .../components/arcam_fmj/translations/cs.json | 13 +++++ .../components/arcam_fmj/translations/ru.json | 2 +- .../components/atag/translations/cs.json | 12 ++++ .../components/august/translations/cs.json | 12 ++++ .../components/awair/translations/cs.json | 26 +++++++++ .../components/awair/translations/it.json | 29 ++++++++++ .../components/axis/translations/cs.json | 12 +++- .../binary_sensor/translations/cs.json | 58 ++++++++++++++++++- .../components/blebox/translations/cs.json | 12 ++++ .../components/blink/translations/cs.json | 19 ++++++ .../components/braviatv/translations/cs.json | 11 ++++ .../components/brother/translations/cs.json | 5 ++ .../components/bsblan/translations/cs.json | 12 ++++ .../cert_expiry/translations/cs.json | 12 ++++ .../components/daikin/translations/cs.json | 22 +++++++ .../components/deconz/translations/cs.json | 6 ++ .../components/denonavr/translations/cs.json | 7 +++ .../components/denonavr/translations/ru.json | 6 +- .../devolo_home_control/translations/cs.json | 11 ++++ .../components/directv/translations/cs.json | 17 ++++++ .../components/doorbird/translations/cs.json | 18 ++++++ .../components/dunehd/translations/cs.json | 18 ++++++ .../components/ecobee/translations/cs.json | 11 ++++ .../components/elgato/translations/cs.json | 12 ++++ .../components/elkm1/translations/cs.json | 12 ++++ .../flick_electric/translations/cs.json | 12 ++++ .../components/flume/translations/cs.json | 12 ++++ .../components/freebox/translations/cs.json | 12 ++++ .../components/fritzbox/translations/cs.json | 19 ++++++ .../components/glances/translations/cs.json | 14 +++++ .../components/gogogate2/translations/cs.json | 19 ++++++ .../components/gogogate2/translations/it.json | 2 +- .../components/gogogate2/translations/no.json | 2 +- .../components/harmony/translations/cs.json | 11 ++++ .../components/heos/translations/cs.json | 11 ++++ .../huawei_lte/translations/it.json | 1 + .../humidifier/translations/cs.json | 8 +++ .../humidifier/translations/it.json | 18 ++++++ .../humidifier/translations/no.json | 12 ++++ .../translations/cs.json | 11 ++++ .../components/iaqualink/translations/cs.json | 12 ++++ .../components/icloud/translations/cs.json | 12 ++++ .../components/ipp/translations/cs.json | 19 ++++++ .../components/isy994/translations/cs.json | 20 +++++++ .../components/konnected/translations/cs.json | 12 ++++ .../components/life360/translations/cs.json | 18 ++++++ .../components/linky/translations/cs.json | 8 +++ .../components/melcloud/translations/cs.json | 12 ++++ .../components/metoffice/translations/cs.json | 11 ++++ .../components/mikrotik/translations/cs.json | 14 +++++ .../components/mill/translations/cs.json | 18 ++++++ .../minecraft_server/translations/cs.json | 11 ++++ .../components/monoprice/translations/cs.json | 11 ++++ .../components/mqtt/translations/cs.json | 21 +++++++ .../components/mqtt/translations/it.json | 32 ++++++++++ .../components/mqtt/translations/no.json | 1 + .../components/myq/translations/cs.json | 12 ++++ .../components/neato/translations/cs.json | 8 +++ .../components/netatmo/translations/cs.json | 17 ++++++ .../components/nexia/translations/cs.json | 12 ++++ .../components/notion/translations/cs.json | 12 ++++ .../components/nuheat/translations/cs.json | 12 ++++ .../components/nut/translations/cs.json | 14 +++++ .../components/nws/translations/cs.json | 11 ++++ .../panasonic_viera/translations/cs.json | 11 ++++ .../components/pi_hole/translations/cs.json | 18 ++++++ .../components/plex/translations/cs.json | 12 ++++ .../plum_lightpad/translations/cs.json | 18 ++++++ .../plum_lightpad/translations/it.json | 18 ++++++ .../components/point/translations/cs.json | 1 + .../components/rachio/translations/cs.json | 11 ++++ .../components/ring/translations/cs.json | 12 ++++ .../components/roku/translations/cs.json | 11 ++++ .../components/roomba/translations/cs.json | 12 ++++ .../components/samsungtv/translations/cs.json | 11 ++++ .../components/sense/translations/cs.json | 12 ++++ .../components/smappee/translations/cs.json | 7 +++ .../smartthings/translations/cs.json | 7 +++ .../components/sms/translations/cs.json | 12 ++++ .../components/sms/translations/it.json | 20 +++++++ .../components/solaredge/translations/cs.json | 11 ++++ .../components/sonarr/translations/cs.json | 21 +++++++ .../components/songpal/translations/cs.json | 10 ++++ .../speedtestdotnet/translations/cs.json | 28 +++++++++ .../squeezebox/translations/cs.json | 27 +++++++++ .../squeezebox/translations/it.json | 33 +++++++++++ .../squeezebox/translations/no.json | 6 ++ .../components/starline/translations/cs.json | 12 ++++ .../synology_dsm/translations/cs.json | 21 +++++++ .../components/tado/translations/cs.json | 12 ++++ .../tellduslive/translations/cs.json | 7 +++ .../components/tesla/translations/cs.json | 12 ++++ .../components/tibber/translations/cs.json | 14 +++++ .../components/tile/translations/cs.json | 12 ++++ .../components/toon/translations/cs.json | 22 +++++++ .../components/toon/translations/it.json | 14 +++++ .../components/toon/translations/no.json | 12 ++++ .../totalconnect/translations/cs.json | 12 ++++ .../transmission/translations/cs.json | 10 ++++ .../components/tuya/translations/cs.json | 25 ++++++++ .../components/vesync/translations/cs.json | 12 ++++ .../components/vilfo/translations/cs.json | 12 ++++ .../components/vizio/translations/cs.json | 22 +++++++ .../components/withings/translations/it.json | 10 +++- .../components/withings/translations/no.json | 2 +- .../components/wled/translations/cs.json | 11 ++++ .../xiaomi_aqara/translations/cs.json | 7 +++ .../xiaomi_aqara/translations/it.json | 40 +++++++++++++ .../xiaomi_aqara/translations/ru.json | 8 +-- .../xiaomi_miio/translations/cs.json | 3 + .../components/zerproc/translations/cs.json | 13 +++++ 116 files changed, 1565 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/agent_dvr/translations/cs.json create mode 100644 homeassistant/components/airly/translations/cs.json create mode 100644 homeassistant/components/airvisual/translations/cs.json create mode 100644 homeassistant/components/ambient_station/translations/cs.json create mode 100644 homeassistant/components/arcam_fmj/translations/cs.json create mode 100644 homeassistant/components/atag/translations/cs.json create mode 100644 homeassistant/components/august/translations/cs.json create mode 100644 homeassistant/components/awair/translations/cs.json create mode 100644 homeassistant/components/awair/translations/it.json create mode 100644 homeassistant/components/blebox/translations/cs.json create mode 100644 homeassistant/components/blink/translations/cs.json create mode 100644 homeassistant/components/braviatv/translations/cs.json create mode 100644 homeassistant/components/bsblan/translations/cs.json create mode 100644 homeassistant/components/cert_expiry/translations/cs.json create mode 100644 homeassistant/components/daikin/translations/cs.json create mode 100644 homeassistant/components/denonavr/translations/cs.json create mode 100644 homeassistant/components/devolo_home_control/translations/cs.json create mode 100644 homeassistant/components/directv/translations/cs.json create mode 100644 homeassistant/components/doorbird/translations/cs.json create mode 100644 homeassistant/components/dunehd/translations/cs.json create mode 100644 homeassistant/components/ecobee/translations/cs.json create mode 100644 homeassistant/components/elgato/translations/cs.json create mode 100644 homeassistant/components/elkm1/translations/cs.json create mode 100644 homeassistant/components/flick_electric/translations/cs.json create mode 100644 homeassistant/components/flume/translations/cs.json create mode 100644 homeassistant/components/freebox/translations/cs.json create mode 100644 homeassistant/components/fritzbox/translations/cs.json create mode 100644 homeassistant/components/glances/translations/cs.json create mode 100644 homeassistant/components/gogogate2/translations/cs.json create mode 100644 homeassistant/components/harmony/translations/cs.json create mode 100644 homeassistant/components/heos/translations/cs.json create mode 100644 homeassistant/components/humidifier/translations/cs.json create mode 100644 homeassistant/components/humidifier/translations/it.json create mode 100644 homeassistant/components/humidifier/translations/no.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/cs.json create mode 100644 homeassistant/components/iaqualink/translations/cs.json create mode 100644 homeassistant/components/icloud/translations/cs.json create mode 100644 homeassistant/components/ipp/translations/cs.json create mode 100644 homeassistant/components/isy994/translations/cs.json create mode 100644 homeassistant/components/konnected/translations/cs.json create mode 100644 homeassistant/components/life360/translations/cs.json create mode 100644 homeassistant/components/melcloud/translations/cs.json create mode 100644 homeassistant/components/metoffice/translations/cs.json create mode 100644 homeassistant/components/mikrotik/translations/cs.json create mode 100644 homeassistant/components/mill/translations/cs.json create mode 100644 homeassistant/components/minecraft_server/translations/cs.json create mode 100644 homeassistant/components/monoprice/translations/cs.json create mode 100644 homeassistant/components/myq/translations/cs.json create mode 100644 homeassistant/components/netatmo/translations/cs.json create mode 100644 homeassistant/components/nexia/translations/cs.json create mode 100644 homeassistant/components/notion/translations/cs.json create mode 100644 homeassistant/components/nuheat/translations/cs.json create mode 100644 homeassistant/components/nut/translations/cs.json create mode 100644 homeassistant/components/nws/translations/cs.json create mode 100644 homeassistant/components/panasonic_viera/translations/cs.json create mode 100644 homeassistant/components/pi_hole/translations/cs.json create mode 100644 homeassistant/components/plex/translations/cs.json create mode 100644 homeassistant/components/plum_lightpad/translations/cs.json create mode 100644 homeassistant/components/plum_lightpad/translations/it.json create mode 100644 homeassistant/components/rachio/translations/cs.json create mode 100644 homeassistant/components/ring/translations/cs.json create mode 100644 homeassistant/components/roomba/translations/cs.json create mode 100644 homeassistant/components/samsungtv/translations/cs.json create mode 100644 homeassistant/components/sense/translations/cs.json create mode 100644 homeassistant/components/smappee/translations/cs.json create mode 100644 homeassistant/components/sms/translations/cs.json create mode 100644 homeassistant/components/sms/translations/it.json create mode 100644 homeassistant/components/solaredge/translations/cs.json create mode 100644 homeassistant/components/sonarr/translations/cs.json create mode 100644 homeassistant/components/songpal/translations/cs.json create mode 100644 homeassistant/components/speedtestdotnet/translations/cs.json create mode 100644 homeassistant/components/squeezebox/translations/cs.json create mode 100644 homeassistant/components/squeezebox/translations/it.json create mode 100644 homeassistant/components/starline/translations/cs.json create mode 100644 homeassistant/components/synology_dsm/translations/cs.json create mode 100644 homeassistant/components/tado/translations/cs.json create mode 100644 homeassistant/components/tesla/translations/cs.json create mode 100644 homeassistant/components/tibber/translations/cs.json create mode 100644 homeassistant/components/tile/translations/cs.json create mode 100644 homeassistant/components/toon/translations/cs.json create mode 100644 homeassistant/components/totalconnect/translations/cs.json create mode 100644 homeassistant/components/tuya/translations/cs.json create mode 100644 homeassistant/components/vesync/translations/cs.json create mode 100644 homeassistant/components/vilfo/translations/cs.json create mode 100644 homeassistant/components/vizio/translations/cs.json create mode 100644 homeassistant/components/wled/translations/cs.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/cs.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/it.json create mode 100644 homeassistant/components/zerproc/translations/cs.json diff --git a/homeassistant/components/adguard/translations/cs.json b/homeassistant/components/adguard/translations/cs.json index fc450c2e908..082fa365e7c 100644 --- a/homeassistant/components/adguard/translations/cs.json +++ b/homeassistant/components/adguard/translations/cs.json @@ -4,6 +4,14 @@ "hassio_confirm": { "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?", "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + }, + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } } } } diff --git a/homeassistant/components/agent_dvr/translations/cs.json b/homeassistant/components/agent_dvr/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/cs.json b/homeassistant/components/airly/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/airly/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json new file mode 100644 index 00000000000..9c41a50b09f --- /dev/null +++ b/homeassistant/components/airvisual/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "geography": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + }, + "node_pro": { + "data": { + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/cs.json b/homeassistant/components/ambient_station/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/ambient_station/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/cs.json b/homeassistant/components/arcam_fmj/translations/cs.json new file mode 100644 index 00000000000..5bcc0c2295d --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + }, + "description": "Zadejte n\u00e1zev hostitele nebo IP adresu za\u0159\u00edzen\u00ed." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant/components/arcam_fmj/translations/ru.json index b02c482c438..807bfb4e4c5 100644 --- a/homeassistant/components/arcam_fmj/translations/ru.json +++ b/homeassistant/components/arcam_fmj/translations/ru.json @@ -15,7 +15,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." } } }, diff --git a/homeassistant/components/atag/translations/cs.json b/homeassistant/components/atag/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/atag/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/cs.json b/homeassistant/components/august/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/august/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/cs.json b/homeassistant/components/awair/translations/cs.json new file mode 100644 index 00000000000..131ef0a6261 --- /dev/null +++ b/homeassistant/components/awair/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n", + "no_devices": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed.", + "reauth_successful": "P\u0159\u00edstupov\u00fd token \u00fasp\u011b\u0161n\u011b aktualizov\u00e1n" + }, + "error": { + "auth": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token" + }, + "step": { + "reauth": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "email": "E-mail" + } + }, + "user": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "email": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json new file mode 100644 index 00000000000..b7f699e7902 --- /dev/null +++ b/homeassistant/components/awair/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "no_devices": "Nessun dispositivo trovato sulla rete", + "reauth_successful": "Token di accesso aggiornato correttamente" + }, + "error": { + "auth": "Token di accesso non valido", + "unknown": "Errore API Awair sconosciuto." + }, + "step": { + "reauth": { + "data": { + "access_token": "Token di accesso", + "email": "E-mail" + }, + "description": "Inserisci nuovamente il tuo token di accesso per sviluppatori Awair." + }, + "user": { + "data": { + "access_token": "Token di accesso", + "email": "E-mail" + }, + "description": "\u00c8 necessario registrarsi per un token di accesso per sviluppatori Awair all'indirizzo: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/cs.json b/homeassistant/components/axis/translations/cs.json index 258f301e432..dc88556b6e1 100644 --- a/homeassistant/components/axis/translations/cs.json +++ b/homeassistant/components/axis/translations/cs.json @@ -1,5 +1,15 @@ { "config": { - "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})" + "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index c3ace898a8b..a656a7274ba 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -1,8 +1,64 @@ { "device_automation": { + "condition_type": { + "is_locked": "{entity_name} je zam\u010deno", + "is_not_bat_low": "{entity_name} baterie v norm\u00e1lu", + "is_not_moving": "{entity_name} se nepohybuje", + "is_not_occupied": "{entity_name} nen\u00ed obsazeno", + "is_not_open": "{entity_name} je zav\u0159eno", + "is_not_plugged_in": "{entity_name} je odpojeno", + "is_not_powered": "{entity_name} nen\u00ed nap\u00e1jeno", + "is_not_present": "{entity_name} nen\u00ed p\u0159\u00edtomno", + "is_not_unsafe": "{entity_name} je bezpe\u010dno", + "is_occupied": "{entity_name} je obsazeno", + "is_off": "{entity_name} je vypnuto", + "is_on": "{entity_name} je zapnuto", + "is_open": "{entity_name} je otev\u0159eno", + "is_plugged_in": "{entity_name} je p\u0159ipojeno", + "is_powered": "{entity_name} nap\u00e1jeno", + "is_present": "{entity_name} p\u0159\u00edtomno", + "is_problem": "{entity_name} detekuje probl\u00e9m", + "is_smoke": "{entity_name} detekuje kou\u0159", + "is_sound": "{entity_name} detekuje zvuk", + "is_unsafe": "{entity_name} nen\u00ed bezpe\u010dno", + "is_vibration": "{entity_name} detekuje vibrace" + }, "trigger_type": { + "bat_low": "{entity_name} vybit\u00e1 baterie", + "cold": "{entity_name} vychladlo", + "connected": "{entity_name} p\u0159ipojeno", + "locked": "{entity_name} zam\u010deno", "moist": "{entity_name} se navlh\u010dil", - "not_opened": "{entity_name} uzav\u0159eno" + "no_gas": "{entity_name} p\u0159estalo detekovat plyn", + "no_light": "{entity_name} p\u0159estalo detekovat sv\u011btlo", + "no_motion": "{entity_name} p\u0159estalo detekovat pohyb", + "no_problem": "{entity_name} p\u0159estalo detekovat probl\u00e9m", + "no_smoke": "{entity_name} p\u0159estalo detekovat kou\u0159", + "no_sound": "{entity_name} p\u0159estalo detekovat zvuk", + "no_vibration": "{entity_name} p\u0159estalo detekovat vibrace", + "not_bat_low": "{entity_name} baterie v norm\u00e1lu", + "not_connected": "{entity_name} odpojeno", + "not_locked": "{entity_name} odem\u010deno", + "not_moist": "{entity_name} vyschlo", + "not_moving": "{entity_name} se p\u0159estalo pohybovat", + "not_occupied": "{entity_name} volno", + "not_opened": "{entity_name} uzav\u0159eno", + "not_plugged_in": "{entity_name} odpojeno", + "not_powered": "{entity_name} nen\u00ed nap\u00e1jeno", + "not_present": "{entity_name} nep\u0159\u00edtomno", + "not_unsafe": "{entity_name} bezpe\u010dno", + "occupied": "{entity_name} obsazeno", + "opened": "{entity_name} otev\u0159eno", + "plugged_in": "{entity_name} p\u0159ipojeno", + "powered": "{entity_name} nap\u00e1jeno", + "present": "{entity_name} p\u0159\u00edtomno", + "problem": "{entity_name} detekuje probl\u00e9m", + "smoke": "{entity_name} detekuje kou\u0159", + "sound": "{entity_name} detekuje zvuk", + "turned_off": "{entity_name} vypnuto", + "turned_on": "{entity_name} zapnuto", + "unsafe": "{entity_name} hl\u00e1s\u00ed ohro\u017een\u00ed", + "vibration": "{entity_name} detekuje vibrace" } }, "state": { diff --git a/homeassistant/components/blebox/translations/cs.json b/homeassistant/components/blebox/translations/cs.json new file mode 100644 index 00000000000..814e0c63418 --- /dev/null +++ b/homeassistant/components/blebox/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP adresa", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/cs.json b/homeassistant/components/blink/translations/cs.json new file mode 100644 index 00000000000..3b0f6c09f82 --- /dev/null +++ b/homeassistant/components/blink/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/cs.json b/homeassistant/components/braviatv/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/braviatv/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/cs.json b/homeassistant/components/brother/translations/cs.json index 716b62c6c70..ed5f4998d3c 100644 --- a/homeassistant/components/brother/translations/cs.json +++ b/homeassistant/components/brother/translations/cs.json @@ -2,6 +2,11 @@ "config": { "flow_title": "Tisk\u00e1rna Brother: {model} {serial_number}", "step": { + "user": { + "data": { + "host": "Hostitel" + } + }, "zeroconf_confirm": { "data": { "type": "Typ tisk\u00e1rny" diff --git a/homeassistant/components/bsblan/translations/cs.json b/homeassistant/components/bsblan/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/bsblan/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ 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..406bddd1a12 --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/cs.json b/homeassistant/components/daikin/translations/cs.json new file mode 100644 index 00000000000..ad0a820e599 --- /dev/null +++ b/homeassistant/components/daikin/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "device_fail": "Neo\u010dek\u00e1van\u00e1 chyba", + "device_timeout": "Nepoda\u0159ilo se p\u0159ipojit", + "forbidden": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "key": "Kl\u00ed\u010d API", + "password": "Heslo" + }, + "description": "Zadejte IP adresu va\u0161eho Daikin AC. \n\n V\u0161imn\u011bte si, \u017ee Kl\u00ed\u010d API a Heslo pou\u017e\u00edvaj\u00ed za\u0159\u00edzen\u00ed BRP072Cxx respektive SKYFi." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 79c173d692a..44d6b098da2 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -17,6 +17,12 @@ "link": { "description": "Odemkn\u011bte br\u00e1nu deCONZ, pro registraci v Home Assistant. \n\n 1. P\u0159ejd\u011bte do nastaven\u00ed syst\u00e9mu deCONZ \n 2. Stiskn\u011bte tla\u010d\u00edtko \"Unlock Gateway\"", "title": "Propojit s deCONZ" + }, + "manual_input": { + "data": { + "host": "Hostitel", + "port": "Port" + } } } } diff --git a/homeassistant/components/denonavr/translations/cs.json b/homeassistant/components/denonavr/translations/cs.json new file mode 100644 index 00000000000..1eee6cfc151 --- /dev/null +++ b/homeassistant/components/denonavr/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json index de9ca15c730..82c6b685b62 100644 --- a/homeassistant/components/denonavr/translations/ru.json +++ b/homeassistant/components/denonavr/translations/ru.json @@ -14,7 +14,7 @@ "step": { "confirm": { "description": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440\u044b Denon" + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" }, "select": { "data": { @@ -28,7 +28,7 @@ "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, "description": "\u0415\u0441\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440\u044b Denon" + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" } } }, @@ -41,7 +41,7 @@ "zone3": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u043e\u043d\u044b 3" }, "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440\u044b Denon" + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" } } } diff --git a/homeassistant/components/devolo_home_control/translations/cs.json b/homeassistant/components/devolo_home_control/translations/cs.json new file mode 100644 index 00000000000..66e8cd431a4 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/cs.json b/homeassistant/components/directv/translations/cs.json new file mode 100644 index 00000000000..8e0810bb333 --- /dev/null +++ b/homeassistant/components/directv/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/cs.json b/homeassistant/components/doorbird/translations/cs.json new file mode 100644 index 00000000000..db55b78e689 --- /dev/null +++ b/homeassistant/components/doorbird/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/cs.json b/homeassistant/components/dunehd/translations/cs.json new file mode 100644 index 00000000000..6b7bad06c1c --- /dev/null +++ b/homeassistant/components/dunehd/translations/cs.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/cs.json b/homeassistant/components/ecobee/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/ecobee/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/cs.json b/homeassistant/components/elgato/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/elgato/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/cs.json b/homeassistant/components/elkm1/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/elkm1/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/cs.json b/homeassistant/components/flick_electric/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/cs.json b/homeassistant/components/flume/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/flume/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/cs.json b/homeassistant/components/freebox/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/freebox/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/cs.json b/homeassistant/components/fritzbox/translations/cs.json new file mode 100644 index 00000000000..4ba0a24fadd --- /dev/null +++ b/homeassistant/components/fritzbox/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/cs.json b/homeassistant/components/glances/translations/cs.json new file mode 100644 index 00000000000..23ddc89a94d --- /dev/null +++ b/homeassistant/components/glances/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/cs.json b/homeassistant/components/gogogate2/translations/cs.json new file mode 100644 index 00000000000..c46cedfa272 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/it.json b/homeassistant/components/gogogate2/translations/it.json index 378d55630a4..7be4c2f5750 100644 --- a/homeassistant/components/gogogate2/translations/it.json +++ b/homeassistant/components/gogogate2/translations/it.json @@ -14,7 +14,7 @@ "password": "Password", "username": "Nome utente" }, - "description": "Fornire le informazioni richieste di seguito.", + "description": "Fornire le informazioni richieste di seguito. Nota: solo l'utente \"admin\" funziona..", "title": "Configurazione GogoGate2" } } diff --git a/homeassistant/components/gogogate2/translations/no.json b/homeassistant/components/gogogate2/translations/no.json index 794c72e9aeb..6619f4c8fe3 100644 --- a/homeassistant/components/gogogate2/translations/no.json +++ b/homeassistant/components/gogogate2/translations/no.json @@ -7,7 +7,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Oppgi n\u00f8dvendig informasjon nedenfor.", + "description": "Gi n\u00f8dvendig informasjon nedenfor. Merk: bare \"admin\" brukeren er kjent for \u00e5 fungere.", "title": "Konfigurer GogoGate2" } } diff --git a/homeassistant/components/harmony/translations/cs.json b/homeassistant/components/harmony/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/harmony/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/cs.json b/homeassistant/components/heos/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/heos/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ 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 9f5a2cf04b2..53a91afab06 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -16,6 +16,7 @@ "response_error": "Errore sconosciuto dal dispositivo", "unknown_connection_error": "Errore sconosciuto durante la connessione al dispositivo" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/humidifier/translations/cs.json b/homeassistant/components/humidifier/translations/cs.json new file mode 100644 index 00000000000..5f05202a2e3 --- /dev/null +++ b/homeassistant/components/humidifier/translations/cs.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "off": "Neaktivn\u00ed", + "on": "Aktivn\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/it.json b/homeassistant/components/humidifier/translations/it.json new file mode 100644 index 00000000000..19b9102fbf3 --- /dev/null +++ b/homeassistant/components/humidifier/translations/it.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Impostare l'umidit\u00e0 per {entity_name}", + "set_mode": "Cambiare la modalit\u00e0 di {entity_name}", + "toggle": "Commuta {entity_name}", + "turn_off": "Disattivare {entity_name}", + "turn_on": "Attivare {entity_name}" + } + }, + "state": { + "_": { + "off": "Spento", + "on": "Acceso" + } + }, + "title": "Umidificatore" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/no.json b/homeassistant/components/humidifier/translations/no.json new file mode 100644 index 00000000000..42caaf0d774 --- /dev/null +++ b/homeassistant/components/humidifier/translations/no.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Angi fuktighet for {entity_name}", + "set_mode": "Endre modus p\u00e5 {entity_name}", + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + } + }, + "title": "Luftfukter" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/cs.json b/homeassistant/components/hunterdouglas_powerview/translations/cs.json new file mode 100644 index 00000000000..5656d8635a0 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/cs.json b/homeassistant/components/iaqualink/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/cs.json b/homeassistant/components/icloud/translations/cs.json new file mode 100644 index 00000000000..e20e44aec6e --- /dev/null +++ b/homeassistant/components/icloud/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/cs.json b/homeassistant/components/ipp/translations/cs.json new file mode 100644 index 00000000000..2048e1a0681 --- /dev/null +++ b/homeassistant/components/ipp/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no", + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json new file mode 100644 index 00000000000..78bf9ea9af1 --- /dev/null +++ b/homeassistant/components/isy994/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/cs.json b/homeassistant/components/konnected/translations/cs.json new file mode 100644 index 00000000000..814e0c63418 --- /dev/null +++ b/homeassistant/components/konnected/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP adresa", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/cs.json b/homeassistant/components/life360/translations/cs.json new file mode 100644 index 00000000000..dc105084202 --- /dev/null +++ b/homeassistant/components/life360/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "user_already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + }, + "error": { + "user_already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/cs.json b/homeassistant/components/linky/translations/cs.json index f914f0f5a1c..8f8c4648d5f 100644 --- a/homeassistant/components/linky/translations/cs.json +++ b/homeassistant/components/linky/translations/cs.json @@ -2,6 +2,14 @@ "config": { "abort": { "already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/cs.json b/homeassistant/components/melcloud/translations/cs.json new file mode 100644 index 00000000000..e20e44aec6e --- /dev/null +++ b/homeassistant/components/melcloud/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/cs.json b/homeassistant/components/metoffice/translations/cs.json new file mode 100644 index 00000000000..bdede2be02a --- /dev/null +++ b/homeassistant/components/metoffice/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/cs.json b/homeassistant/components/mikrotik/translations/cs.json new file mode 100644 index 00000000000..23ddc89a94d --- /dev/null +++ b/homeassistant/components/mikrotik/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/cs.json b/homeassistant/components/mill/translations/cs.json new file mode 100644 index 00000000000..deeab584855 --- /dev/null +++ b/homeassistant/components/mill/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + }, + "error": { + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/cs.json b/homeassistant/components/minecraft_server/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/cs.json b/homeassistant/components/monoprice/translations/cs.json new file mode 100644 index 00000000000..892b8b2cd91 --- /dev/null +++ b/homeassistant/components/monoprice/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index b49bc8cf343..95bd6d68c8f 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -25,5 +25,26 @@ "title": "MQTT Broker prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" } } + }, + "options": { + "error": { + "cannot_connect": "Nelze se p\u0159ipojit k brokeru." + }, + "step": { + "broker": { + "data": { + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "description": "Zadejte informace proo p\u0159ipojen\u00ed zprost\u0159edkovatele protokolu MQTT." + }, + "options": { + "data": { + "discovery": "Povolit zji\u0161\u0165ov\u00e1n\u00ed" + }, + "description": "Zvolte mo\u017enosti MQTT." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 3506049abce..ef5d173dce7 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -47,5 +47,37 @@ "button_short_release": "\"{subtype}\" rilasciato", "button_triple_press": "\"{subtype}\" cliccato tre volte" } + }, + "options": { + "error": { + "bad_birth": "Argomento birth non valido.", + "bad_will": "Argomento will non valido.", + "cannot_connect": "Impossibile connettersi al broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "description": "Inserisci le informazioni di connessione del tuo broker MQTT." + }, + "options": { + "data": { + "birth_payload": "Payload del messaggio birth", + "birth_qos": "QoS del messaggio birth", + "birth_retain": "Persistenza del messaggio birth", + "birth_topic": "Argomento del messaggio birth", + "discovery": "Attiva l'individuazione", + "will_payload": "Payload del messaggio will", + "will_qos": "QoS del messaggio will", + "will_retain": "Persistenza del messaggio will", + "will_topic": "Argomento del messaggio will" + }, + "description": "Selezionare le opzioni MQTT." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 1871b2bffe7..d78922b0118 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -63,6 +63,7 @@ }, "options": { "data": { + "birth_payload": "Birth message payload", "birth_qos": "F\u00f8dselsmelding QoS", "birth_retain": "F\u00f8dselsmelding beholder", "birth_topic": "F\u00f8dselsmeldingsemne", diff --git a/homeassistant/components/myq/translations/cs.json b/homeassistant/components/myq/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/myq/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/cs.json b/homeassistant/components/neato/translations/cs.json index 2a448a26e40..a3c86fa605f 100644 --- a/homeassistant/components/neato/translations/cs.json +++ b/homeassistant/components/neato/translations/cs.json @@ -2,6 +2,14 @@ "config": { "error": { "unexpected_error": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/cs.json b/homeassistant/components/netatmo/translations/cs.json new file mode 100644 index 00000000000..5c5b19b0bf8 --- /dev/null +++ b/homeassistant/components/netatmo/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Ji\u017e nakonfigurov\u00e1no. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Komponenta nen\u00ed nakonfigurov\u00e1na. Postupujte podle dokumentace." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/cs.json b/homeassistant/components/nexia/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/nexia/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/cs.json b/homeassistant/components/notion/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/notion/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/cs.json b/homeassistant/components/nuheat/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/nuheat/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/cs.json b/homeassistant/components/nut/translations/cs.json new file mode 100644 index 00000000000..23ddc89a94d --- /dev/null +++ b/homeassistant/components/nut/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/cs.json b/homeassistant/components/nws/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/nws/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/cs.json b/homeassistant/components/panasonic_viera/translations/cs.json new file mode 100644 index 00000000000..5656d8635a0 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/cs.json b/homeassistant/components/pi_hole/translations/cs.json new file mode 100644 index 00000000000..fbf7317b4f0 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nakonfigurov\u00e1na" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/cs.json b/homeassistant/components/plex/translations/cs.json new file mode 100644 index 00000000000..4ea95b97a9b --- /dev/null +++ b/homeassistant/components/plex/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "manual_setup": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/cs.json b/homeassistant/components/plum_lightpad/translations/cs.json new file mode 100644 index 00000000000..e530ca166e5 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nelze se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/it.json b/homeassistant/components/plum_lightpad/translations/it.json new file mode 100644 index 00000000000..8ed082face7 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/cs.json b/homeassistant/components/point/translations/cs.json index f9c802c2612..17a333a660d 100644 --- a/homeassistant/components/point/translations/cs.json +++ b/homeassistant/components/point/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "Ji\u017e nakonfigurov\u00e1no. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "external_setup": "Point \u00fasp\u011b\u0161n\u011b nakonfigurov\u00e1n z jin\u00e9ho toku.", diff --git a/homeassistant/components/rachio/translations/cs.json b/homeassistant/components/rachio/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/rachio/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/cs.json b/homeassistant/components/ring/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/ring/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/cs.json b/homeassistant/components/roku/translations/cs.json index 3b814303e69..70374eaa47f 100644 --- a/homeassistant/components/roku/translations/cs.json +++ b/homeassistant/components/roku/translations/cs.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/cs.json b/homeassistant/components/roomba/translations/cs.json new file mode 100644 index 00000000000..6d21c8f19c4 --- /dev/null +++ b/homeassistant/components/roomba/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/cs.json b/homeassistant/components/samsungtv/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/cs.json b/homeassistant/components/sense/translations/cs.json new file mode 100644 index 00000000000..f019fb3fce6 --- /dev/null +++ b/homeassistant/components/sense/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/cs.json b/homeassistant/components/smappee/translations/cs.json new file mode 100644 index 00000000000..36ec311b7c3 --- /dev/null +++ b/homeassistant/components/smappee/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nakonfigurov\u00e1no. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/cs.json b/homeassistant/components/smartthings/translations/cs.json index 60a0ba0e158..70c4571c3e0 100644 --- a/homeassistant/components/smartthings/translations/cs.json +++ b/homeassistant/components/smartthings/translations/cs.json @@ -6,6 +6,13 @@ "token_invalid_format": "Token mus\u00ed b\u00fdt ve form\u00e1tu UID/GUID.", "token_unauthorized": "Token je neplatn\u00fd nebo ji\u017e nen\u00ed autorizov\u00e1n.", "webhook_error": "SmartThings nemohly ov\u011b\u0159it koncov\u00fd bod nakonfigurovan\u00fd v `base_url`. P\u0159e\u010dt\u011bte si pros\u00edm po\u017eadavky na komponenty." + }, + "step": { + "pat": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sms/translations/cs.json b/homeassistant/components/sms/translations/cs.json new file mode 100644 index 00000000000..47c55692a99 --- /dev/null +++ b/homeassistant/components/sms/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no", + "single_instance_allowed": "Ji\u017e nakonfigurov\u00e1no. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/it.json b/homeassistant/components/sms/translations/it.json new file mode 100644 index 00000000000..77098f93470 --- /dev/null +++ b/homeassistant/components/sms/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "device": "Dispositivo" + }, + "title": "Connettersi al modem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/cs.json b/homeassistant/components/solaredge/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/solaredge/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/cs.json b/homeassistant/components/sonarr/translations/cs.json new file mode 100644 index 00000000000..5552f5ae6a7 --- /dev/null +++ b/homeassistant/components/sonarr/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nakonfigurov\u00e1na", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/cs.json b/homeassistant/components/songpal/translations/cs.json new file mode 100644 index 00000000000..51335918cf6 --- /dev/null +++ b/homeassistant/components/songpal/translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/cs.json b/homeassistant/components/speedtestdotnet/translations/cs.json new file mode 100644 index 00000000000..26927f4e6f3 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Povolena je pouze jedna instance.", + "wrong_server_id": "Id serveru nen\u00ed platn\u00e9." + }, + "step": { + "user": { + "description": "Opravdu chcete nastavit SpeedTest?", + "title": "Nastavit SpeedTest" + } + } + }, + "options": { + "error": { + "retrive_error": "P\u0159i na\u010d\u00edt\u00e1n\u00ed seznamu server\u016f do\u0161lo k chyb\u011b" + }, + "step": { + "init": { + "data": { + "manual": "Zak\u00e1zat automatickou aktualizaci", + "scan_interval": "Frekvence aktualizace (minuty)", + "server_name": "Vyberte testovac\u00ed server" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/cs.json b/homeassistant/components/squeezebox/translations/cs.json new file mode 100644 index 00000000000..0bf738d91ff --- /dev/null +++ b/homeassistant/components/squeezebox/translations/cs.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "edit": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/it.json b/homeassistant/components/squeezebox/translations/it.json new file mode 100644 index 00000000000..08918192b7a --- /dev/null +++ b/homeassistant/components/squeezebox/translations/it.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "no_server_found": "Nessun server LMS trovato." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "no_server_found": "Impossibile rilevare automaticamente il server.", + "unknown": "Errore imprevisto" + }, + "flow_title": "Logitech Squeezebox: {host}", + "step": { + "edit": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "title": "Modificare le informazioni di connessione" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Configurare Logitech Media Server" + } + } + }, + "title": "Logitech Squeezebox" +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/no.json b/homeassistant/components/squeezebox/translations/no.json index aa71e9a0c52..030e513deb2 100644 --- a/homeassistant/components/squeezebox/translations/no.json +++ b/homeassistant/components/squeezebox/translations/no.json @@ -3,7 +3,13 @@ "abort": { "no_server_found": "Ingen LMS-server funnet." }, + "error": { + "no_server_found": "Kan ikke automatisk oppdage serveren." + }, "step": { + "edit": { + "title": "Redigere tilkoblingsinformasjon" + }, "user": { "title": "Konfigurer Logitech Media Server" } diff --git a/homeassistant/components/starline/translations/cs.json b/homeassistant/components/starline/translations/cs.json new file mode 100644 index 00000000000..94c158b64d4 --- /dev/null +++ b/homeassistant/components/starline/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "auth_user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/cs.json b/homeassistant/components/synology_dsm/translations/cs.json new file mode 100644 index 00000000000..57dc028c0f4 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "link": { + "data": { + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/cs.json b/homeassistant/components/tado/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/tado/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/cs.json b/homeassistant/components/tellduslive/translations/cs.json index bab99c32124..63813e9c856 100644 --- a/homeassistant/components/tellduslive/translations/cs.json +++ b/homeassistant/components/tellduslive/translations/cs.json @@ -2,6 +2,13 @@ "config": { "abort": { "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/cs.json b/homeassistant/components/tesla/translations/cs.json new file mode 100644 index 00000000000..e20e44aec6e --- /dev/null +++ b/homeassistant/components/tesla/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/cs.json b/homeassistant/components/tibber/translations/cs.json new file mode 100644 index 00000000000..d21dc20e8a1 --- /dev/null +++ b/homeassistant/components/tibber/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token" + }, + "step": { + "user": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/cs.json b/homeassistant/components/tile/translations/cs.json new file mode 100644 index 00000000000..e20e44aec6e --- /dev/null +++ b/homeassistant/components/tile/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/cs.json b/homeassistant/components/toon/translations/cs.json new file mode 100644 index 00000000000..ad1cdd5e76f --- /dev/null +++ b/homeassistant/components/toon/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Komponenta nen\u00ed nakonfigurov\u00e1na. Postupujte podle dokumentace." + }, + "step": { + "agreement": { + "title": "Vyberte si smlouvu" + }, + "authenticate": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "pick_implementation": { + "title": "Zvolte pomoc\u00ed kter\u00e9ho poskytovatele chcete slu\u017ebu ov\u011b\u0159it." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index af7d61d0ef9..5e49e13bd7e 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "L'accordo selezionato \u00e8 gi\u00e0 configurato.", + "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "client_id": "L'ID client dalla configurazione non \u00e8 valido.", "client_secret": "Il client segreto della configurazione non \u00e8 valido.", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_agreements": "Questo account non ha display Toon.", "no_app": "\u00c8 necessario configurare Toon prima di poter eseguire l'autenticazione con esso. [Si prega di leggere le istruzioni](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Si \u00e8 verificato un errore imprevisto durante l'autenticazione." @@ -12,6 +16,13 @@ "display_exists": "Il display selezionato \u00e8 gi\u00e0 configurato." }, "step": { + "agreement": { + "data": { + "agreement": "Accordo" + }, + "description": "Selezionare l'indirizzo del contratto che si desidera aggiungere.", + "title": "Seleziona il tuo contratto" + }, "authenticate": { "data": { "password": "Password", @@ -27,6 +38,9 @@ }, "description": "Seleziona il display Toon con cui connettersi.", "title": "Seleziona il display" + }, + "pick_implementation": { + "title": "Scegliere il tenant con cui eseguire l'autenticazione" } } } diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index 89669e9b019..1aa78d734c1 100644 --- a/homeassistant/components/toon/translations/no.json +++ b/homeassistant/components/toon/translations/no.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "Den valgte avtalen er allerede konfigurert.", + "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", "client_id": "Klient ID fra konfigurasjonen er ugyldig.", "client_secret": "Klient hemmeligheten fra konfigurasjonen er ugyldig.", "no_agreements": "Denne kontoen har ingen Toon skjermer.", @@ -12,6 +14,13 @@ "display_exists": "Den valgte skjermen er allerede konfigurert." }, "step": { + "agreement": { + "data": { + "agreement": "Avtale" + }, + "description": "Velg avtalsadressen du vil legge til.", + "title": "Velg din avtale" + }, "authenticate": { "data": { "password": "Passord", @@ -27,6 +36,9 @@ }, "description": "Velg Toon skjerm \u00e5 koble til.", "title": "Velg skjerm" + }, + "pick_implementation": { + "title": "Velg din leietaker til \u00e5 autentisere med" } } } diff --git a/homeassistant/components/totalconnect/translations/cs.json b/homeassistant/components/totalconnect/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/cs.json b/homeassistant/components/transmission/translations/cs.json index bb96d5809e1..9c3d253e8e0 100644 --- a/homeassistant/components/transmission/translations/cs.json +++ b/homeassistant/components/transmission/translations/cs.json @@ -5,6 +5,16 @@ }, "error": { "name_exists": "Jm\u00e9no ji\u017e existuje" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } }, "options": { diff --git a/homeassistant/components/tuya/translations/cs.json b/homeassistant/components/tuya/translations/cs.json new file mode 100644 index 00000000000..0dadb787d3a --- /dev/null +++ b/homeassistant/components/tuya/translations/cs.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "auth_failed": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "conn_error": "Nepoda\u0159ilo se p\u0159ipojit", + "single_instance_allowed": "Ji\u017e nakonfigurov\u00e1no. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "auth_failed": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "flow_title": "Konfigurace Tuya", + "step": { + "user": { + "data": { + "country_code": "K\u00f3d zem\u011b va\u0161eho \u00fa\u010dtu (nap\u0159. 1 pro USA nebo 86 pro \u010c\u00ednu)", + "password": "Heslo", + "platform": "Aplikace, ve kter\u00e9 m\u00e1te zaregistrovan\u00fd \u00fa\u010det", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "description": "Zadejte sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje k Tuya.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/cs.json b/homeassistant/components/vesync/translations/cs.json new file mode 100644 index 00000000000..e20e44aec6e --- /dev/null +++ b/homeassistant/components/vesync/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/cs.json b/homeassistant/components/vilfo/translations/cs.json new file mode 100644 index 00000000000..f1b0c890a22 --- /dev/null +++ b/homeassistant/components/vilfo/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/cs.json b/homeassistant/components/vizio/translations/cs.json new file mode 100644 index 00000000000..5918b2833bd --- /dev/null +++ b/homeassistant/components/vizio/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "pairing_complete_import": { + "description": "V\u00e1\u0161 je nyn\u00ed p\u0159ipojen k Home Assistant. \n\n V\u00e1\u0161 P\u0159\u00edstupov\u00fd token je '** {access_token} **'." + }, + "user": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "host": "Hostitel" + }, + "description": "P\u0159\u00edstupov\u00fd token je pot\u0159eba pouze pro televizory. Pokud konfigurujete televizor a nem\u00e1te [%key:common:common::config_flow::data::access_token%], ponechte jej pr\u00e1zdn\u00e9, abyste mohli proj\u00edt procesem p\u00e1rov\u00e1n\u00ed." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index f6566ccab77..7302797c37c 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -1,12 +1,16 @@ { "config": { "abort": { + "already_configured": "Configurazione aggiornata per il profilo.", "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", "missing_configuration": "Il componente Withings non \u00e8 configurato. Si prega di seguire la documentazione." }, "create_entry": { "default": "Autenticazione riuscita con Withings." }, + "error": { + "profile_exists": "Il profilo utente \u00e8 gi\u00e0 configurato. Si prega di fornire un nome di profilo univoco." + }, "flow_title": "Withings: {profile}", "step": { "pick_implementation": { @@ -14,14 +18,14 @@ }, "profile": { "data": { - "profile": "Profilo" + "profile": "Nome del 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.", + "description": "Fornire un nome di profilo univoco per questi dati. Di solito questo \u00e8 il nome del profilo selezionato nella fase precedente.", "title": "Profilo utente." }, "reauth": { "description": "Il profilo \"{profile}\" deve essere autenticato nuovamente per continuare a ricevere i dati Withings.", - "title": "Riautentica {profile}" + "title": "Riautenticare il profilo" } } } diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 1581d073ed9..6e5af1fb33c 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -25,7 +25,7 @@ }, "reauth": { "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", - "title": "Re-autentisere {profil}" + "title": "Autentiser profil p\u00e5 nytt" } } } diff --git a/homeassistant/components/wled/translations/cs.json b/homeassistant/components/wled/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/wled/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/cs.json b/homeassistant/components/xiaomi_aqara/translations/cs.json new file mode 100644 index 00000000000..1eee6cfc151 --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json new file mode 100644 index 00000000000..bc64862afec --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione per questo gateway \u00e8 gi\u00e0 in corso", + "not_xiaomi_aqara": "Non \u00e8 un Gateway Xiaomi Aqara, il dispositivo scoperto non corrisponde ai gateway noti" + }, + "error": { + "discovery_error": "Impossibile individuare un gateway Xiaomi Aqara, provare a utilizzare l'IP del dispositivo che esegue HomeAssistant come interfaccia", + "invalid_interface": "Interfaccia di rete non valida", + "invalid_key": "Chiave gateway non valida", + "not_found_error": "Zeroconf ha scoperto che non \u00e8 stato possibile trovare il gateway per ottenere le informazioni necessarie, provare a utilizzare l'IP del dispositivo che esegue HomeAssistant come interfaccia" + }, + "flow_title": "Xiaomi Aqara Gateway: {name}", + "step": { + "select": { + "data": { + "select_ip": "IP gateway" + }, + "description": "Eseguire nuovamente l'installazione se si desidera connettere gateway adizionali", + "title": "Selezionare il Gateway Xiaomi Aqara che si desidera collegare" + }, + "settings": { + "data": { + "key": "La chiave del tuo gateway", + "name": "Nome del Gateway" + }, + "description": "La chiave (password) pu\u00f2 essere recuperata utilizzando questo tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se la chiave non viene fornita, saranno accessibili solo i sensori", + "title": "Xiaomi Aqara Gateway, impostazioni opzionali" + }, + "user": { + "data": { + "interface": "L'interfaccia di rete da utilizzare" + }, + "description": "Connettiti al tuo Xiaomi Aqara Gateway", + "title": "Xiaomi Aqara Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 5a088f0783c..70c19bf2aec 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -11,7 +11,7 @@ "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0430.", "not_found_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0448\u043b\u044e\u0437\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 HomeAssistant \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430." }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara: {name}", "step": { "select": { "data": { @@ -26,14 +26,14 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "description": "\u041a\u043b\u044e\u0447 (\u043f\u0430\u0440\u043e\u043b\u044c) \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0433\u043e \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0430: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u0415\u0441\u043b\u0438 \u043a\u043b\u044e\u0447 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0438.", - "title": "Xiaomi Aqara Gateway" + "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" }, "user": { "data": { "interface": "\u0421\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Xiaomi Aqara Gateway.", - "title": "Xiaomi Aqara Gateway" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u043e \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara.", + "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/cs.json b/homeassistant/components/xiaomi_miio/translations/cs.json index 9064eacf899..d784fda2c0d 100644 --- a/homeassistant/components/xiaomi_miio/translations/cs.json +++ b/homeassistant/components/xiaomi_miio/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, "error": { "connect_error": "Nepoda\u0159ilo se p\u0159ipojit, zkuste to znovu", "no_device_selected": "Nebylo vybr\u00e1no \u017e\u00e1dn\u00e9 za\u0159\u00edzen\u00ed, vyberte jedno za\u0159\u00edzen\u00ed." diff --git a/homeassistant/components/zerproc/translations/cs.json b/homeassistant/components/zerproc/translations/cs.json new file mode 100644 index 00000000000..64fd9e8590c --- /dev/null +++ b/homeassistant/components/zerproc/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed.", + "single_instance_allowed": "Ji\u017e nakonfigurov\u00e1no. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "confirm": { + "description": "Chcete spustit nastaven\u00ed?" + } + } + } +} \ No newline at end of file From ec13eecc59f916221cc781ea113ef59596181827 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 27 Jun 2020 04:03:51 -0400 Subject: [PATCH 037/428] Update Plex tests to mock websockets (#37147) * Update Plex tests to mock websockets * Avoid unnecessary class mock --- tests/components/plex/helpers.py | 8 ++--- tests/components/plex/test_config_flow.py | 29 ++++++++---------- tests/components/plex/test_init.py | 18 +++++++----- tests/components/plex/test_server.py | 36 +++++++++++++++-------- 4 files changed, 50 insertions(+), 41 deletions(-) diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py index 462bd2a5061..8055ab0d5b1 100644 --- a/tests/components/plex/helpers.py +++ b/tests/components/plex/helpers.py @@ -1,9 +1,7 @@ """Helper methods for Plex tests.""" -from homeassistant.components.plex.const import DOMAIN, WEBSOCKETS -def trigger_plex_update(hass, plex_server): +def trigger_plex_update(mock_websocket): """Call the websocket callback method.""" - server_id = plex_server.machineIdentifier - websocket = hass.data[DOMAIN][WEBSOCKETS][server_id] - websocket.callback() + callback = mock_websocket.call_args[0][1] + callback() diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 87183c8f2ad..5ca973f8561 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -367,13 +367,11 @@ async def test_option_flow(hass): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: + ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert mock_listen.called - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED @@ -417,13 +415,11 @@ async def test_missing_option_flow(hass): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: + ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert mock_listen.called - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED @@ -471,17 +467,17 @@ async def test_option_flow_new_users_available(hass, caplog): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + trigger_plex_update(mock_websocket) + await hass.async_block_till_done() + server_id = mock_plex_server.machineIdentifier - - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - trigger_plex_update(hass, mock_plex_server) - await hass.async_block_till_done() - monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users new_users = [x for x in mock_plex_server.accounts if x not in monitored_users] @@ -676,7 +672,7 @@ async def test_manual_config(hass): assert result["errors"]["base"] == "ssl_error" with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" + "homeassistant.components.plex.PlexWebsocket", autospec=True ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MANUAL_SERVER @@ -711,7 +707,7 @@ async def test_manual_config_with_token(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( "plexapi.server.PlexServer", return_value=mock_plex_server - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} ) @@ -742,13 +738,12 @@ async def test_setup_with_limited_credentials(hass): ) as mock_accounts, patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() ), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "homeassistant.components.plex.PlexWebsocket", autospec=True + ): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert mock_listen.called assert mock_accounts.called plex_server = hass.data[DOMAIN][SERVERS][mock_plex_server.machineIdentifier] diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 6902fb4401f..fa60f4dd7d2 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -163,7 +163,9 @@ async def test_setup_with_photo_session(hass): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -171,7 +173,7 @@ async def test_setup_with_photo_session(hass): assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() media_player = hass.states.get("media_player.plex_product_title") @@ -261,15 +263,15 @@ async def test_tokenless_server(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state == ENTRY_STATE_LOADED - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() @@ -286,12 +288,14 @@ async def test_bad_token_with_tokenless_server(hass): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state == ENTRY_STATE_LOADED - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index f678fb30183..7da20846599 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -57,14 +57,16 @@ async def test_new_users_available(hass): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() server_id = mock_plex_server.machineIdentifier - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -96,14 +98,16 @@ async def test_new_ignored_users_available(hass, caplog): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() server_id = mock_plex_server.machineIdentifier - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -139,7 +143,9 @@ async def test_network_error_during_refresh(hass, caplog): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -147,7 +153,7 @@ async def test_network_error_during_refresh(hass, caplog): server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() sensor = hass.states.get("sensor.plex_plex_server_1") @@ -175,7 +181,9 @@ async def test_mark_sessions_idle(hass): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -183,7 +191,7 @@ async def test_mark_sessions_idle(hass): server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() sensor = hass.states.get("sensor.plex_plex_server_1") @@ -216,12 +224,14 @@ async def test_ignore_plex_web_client(hass): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0) - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() sensor = hass.states.get("sensor.plex_plex_server_1") @@ -246,7 +256,9 @@ async def test_media_lookups(hass): with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + ), patch( + "homeassistant.components.plex.PlexWebsocket", autospec=True + ) as mock_websocket: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -255,7 +267,7 @@ async def test_media_lookups(hass): loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Plex Key searches - trigger_plex_update(hass, mock_plex_server) + trigger_plex_update(mock_websocket) await hass.async_block_till_done() media_player_id = hass.states.async_entity_ids("media_player")[0] From 4fd27e879ee21177dbdfac328e21f4f253aec551 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 27 Jun 2020 12:40:34 -0400 Subject: [PATCH 038/428] add phillips remote cluster (#37172) --- .../components/zha/core/channels/manufacturerspecific.py | 8 ++++++++ homeassistant/components/zha/core/registries.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 208bc8f8836..cc56c10d06a 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -39,6 +39,14 @@ class OsramButton(ZigbeeChannel): REPORT_CONFIG = [] +@registries.CHANNEL_ONLY_CLUSTERS.register(registries.PHILLIPS_REMOTE_CLUSTER) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.PHILLIPS_REMOTE_CLUSTER) +class PhillipsRemote(ZigbeeChannel): + """Phillips remote channel.""" + + REPORT_CONFIG = [] + + @registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0) @registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0) class OppleRemote(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index c9b3435482b..0df3b1070bf 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -24,6 +24,8 @@ from .typing import ChannelType GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH, FAN] +PHILLIPS_REMOTE_CLUSTER = 0xFC00 + SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 From 07aba747577c68d859f5a35dfe4407beaa68c226 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 27 Jun 2020 09:45:34 -0700 Subject: [PATCH 039/428] Bump teslajsonpy to 0.9.0 (#37162) --- homeassistant/components/tesla/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 39aa00cfb60..9a0d80f9a05 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -3,6 +3,6 @@ "name": "Tesla", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.8.1"], + "requirements": ["teslajsonpy==0.9.0"], "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/requirements_all.txt b/requirements_all.txt index 85e5c215209..beeb837a4e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2094,7 +2094,7 @@ temperusb==1.5.3 tesla-powerwall==0.2.11 # homeassistant.components.tesla -teslajsonpy==0.8.1 +teslajsonpy==0.9.0 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc388191b45..c5223644ba3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -890,7 +890,7 @@ tellduslive==0.10.11 tesla-powerwall==0.2.11 # homeassistant.components.tesla -teslajsonpy==0.8.1 +teslajsonpy==0.9.0 # homeassistant.components.toon toonapi==0.1.0 From 4acc6f333efe1d2426fb74510ce2de95971205b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Jun 2020 16:46:45 -0500 Subject: [PATCH 040/428] Improve scalability of state change event routing (#37174) --- homeassistant/components/automation/state.py | 5 +- homeassistant/helpers/event.py | 90 ++++++++++++++++++-- tests/components/group/test_init.py | 13 ++- tests/helpers/test_event.py | 86 +++++++++++++++++++ 4 files changed, 183 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 29aea64c9c5..fe49e1cf532 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -6,12 +6,13 @@ from typing import Dict import voluptuous as vol from homeassistant import exceptions -from homeassistant.const import CONF_FOR, CONF_PLATFORM, EVENT_STATE_CHANGED, MATCH_ALL +from homeassistant.const import CONF_FOR, CONF_PLATFORM, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import ( Event, async_track_same_state, + async_track_state_change_event, process_state_match, ) @@ -153,7 +154,7 @@ async def async_attach_trigger( hass, period[entity], call_action, _check_same_state, entity_ids=entity, ) - unsub = hass.bus.async_listen(EVENT_STATE_CHANGED, state_automation_listener) + unsub = async_track_state_change_event(hass, entity_id, state_automation_listener) @callback def async_remove(): diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 266cb150e0a..46c36205cee 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,7 +1,7 @@ """Helpers for listening to events.""" from datetime import datetime, timedelta import functools as ft -from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Union, cast +from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Union import attr @@ -21,6 +21,9 @@ from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe +TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" +TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" + # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name @@ -81,12 +84,6 @@ def async_track_state_change( @callback def state_change_listener(event: Event) -> None: """Handle specific state changes.""" - 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") if old_state is not None: old_state = old_state.state @@ -103,12 +100,91 @@ def async_track_state_change( event.data.get("new_state"), ) + if entity_ids != MATCH_ALL: + # If we have a list of entity ids we use + # async_track_state_change_event to route + # by entity_id to avoid iterating though state change + # events and creating a jobs where the most + # common outcome is to return right away because + # the entity_id does not match since usually + # only one or two listeners want that specific + # entity_id. + return async_track_state_change_event(hass, entity_ids, state_change_listener) + return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener) track_state_change = threaded_listener_factory(async_track_state_change) +@bind_hass +def async_track_state_change_event( + hass: HomeAssistant, entity_ids: Iterable[str], action: Callable[[Event], None] +) -> Callable[[], None]: + """Track specific state change events indexed by entity_id. + + Unlike async_track_state_change, async_track_state_change_event + passes the full event to the callback. + + In order to avoid having to iterate a long list + of EVENT_STATE_CHANGED and fire and create a job + for each one, we keep a dict of entity ids that + care about the state change events so we can + do a fast dict lookup to route events. + """ + + entity_callbacks = hass.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {}) + + if TRACK_STATE_CHANGE_LISTENER not in hass.data: + + @callback + def _async_state_change_dispatcher(event: Event) -> None: + """Dispatch state changes by entity_id.""" + entity_id = event.data.get("entity_id") + + if entity_id not in entity_callbacks: + return + + for action in entity_callbacks[entity_id]: + hass.async_run_job(action, event) + + hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen( + EVENT_STATE_CHANGED, _async_state_change_dispatcher + ) + + entity_ids = [entity_id.lower() for entity_id in entity_ids] + + for entity_id in entity_ids: + if entity_id not in entity_callbacks: + entity_callbacks[entity_id] = [] + + entity_callbacks[entity_id].append(action) + + @callback + def remove_listener() -> None: + """Remove state change listener.""" + _async_remove_state_change_listeners(hass, entity_ids, action) + + return remove_listener + + +@callback +def _async_remove_state_change_listeners( + hass: HomeAssistant, entity_ids: Iterable[str], action: Callable[[Event], None] +) -> None: + """Remove a listener.""" + entity_callbacks = hass.data[TRACK_STATE_CHANGE_CALLBACKS] + + for entity_id in entity_ids: + entity_callbacks[entity_id].remove(action) + if len(entity_callbacks[entity_id]) == 0: + del entity_callbacks[entity_id] + + if not entity_callbacks: + hass.data[TRACK_STATE_CHANGE_LISTENER]() + del hass.data[TRACK_STATE_CHANGE_LISTENER] + + @callback @bind_hass def async_track_template( diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index ff5f3a30f75..921b810fe39 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNKNOWN, ) +from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS from homeassistant.setup import async_setup_component, setup_component from tests.async_mock import patch @@ -390,7 +391,12 @@ class TestComponentsGroup(unittest.TestCase): "group.second_group", "group.test_group", ] - assert self.hass.bus.listeners["state_changed"] == 3 + assert self.hass.bus.listeners["state_changed"] == 1 + assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["hello.world"]) == 1 + assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["sensor.happy"]) == 1 + assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1 + assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1 + assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1 with patch( "homeassistant.config.load_yaml_config_file", @@ -405,7 +411,10 @@ class TestComponentsGroup(unittest.TestCase): "group.all_tests", "group.hello", ] - assert self.hass.bus.listeners["state_changed"] == 2 + assert self.hass.bus.listeners["state_changed"] == 1 + assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1 + assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1 + assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1 def test_modify_group(self): """Test modifying a group.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 654cf8483db..95a093d59ab 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,6 +15,7 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_same_state, async_track_state_change, + async_track_state_change_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -163,6 +164,91 @@ async def test_track_state_change(hass): assert len(wildercard_runs) == 6 +async def test_async_track_state_change_event(hass): + """Test async_track_state_change_event.""" + single_entity_id_tracker = [] + multiple_entity_id_tracker = [] + + @ha.callback + def single_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + single_entity_id_tracker.append((old_state, new_state)) + + @ha.callback + def multiple_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + multiple_entity_id_tracker.append((old_state, new_state)) + + unsub_single = async_track_state_change_event( + hass, ["light.Bowl"], single_run_callback + ) + unsub_multi = async_track_state_change_event( + hass, ["light.Bowl", "switch.kitchen"], multiple_run_callback + ) + + # Adding state to state machine + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert single_entity_id_tracker[-1][0] is None + assert single_entity_id_tracker[-1][1] is not None + assert len(multiple_entity_id_tracker) == 1 + assert multiple_entity_id_tracker[-1][0] is None + assert multiple_entity_id_tracker[-1][1] is not None + + # Set same state should not trigger a state change/listener + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(multiple_entity_id_tracker) == 1 + + # State change off -> on + hass.states.async_set("light.Bowl", "off") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 2 + assert len(multiple_entity_id_tracker) == 2 + + # State change off -> off + hass.states.async_set("light.Bowl", "off", {"some_attr": 1}) + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 3 + assert len(multiple_entity_id_tracker) == 3 + + # State change off -> on + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 4 + assert len(multiple_entity_id_tracker) == 4 + + hass.states.async_remove("light.bowl") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 5 + assert single_entity_id_tracker[-1][0] is not None + assert single_entity_id_tracker[-1][1] is None + assert len(multiple_entity_id_tracker) == 5 + assert multiple_entity_id_tracker[-1][0] is not None + assert multiple_entity_id_tracker[-1][1] is None + + # Set state for different entity id + hass.states.async_set("switch.kitchen", "on") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 5 + assert len(multiple_entity_id_tracker) == 6 + + unsub_single() + # Ensure unsubing the listener works + hass.states.async_set("light.Bowl", "off") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 5 + assert len(multiple_entity_id_tracker) == 7 + + unsub_multi() + + async def test_track_template(hass): """Test tracking template.""" specific_runs = [] From c1194c90cbfbddb17a8faf48c9c38f53cd92c6ca Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 28 Jun 2020 00:05:05 +0000 Subject: [PATCH 041/428] [ci skip] Translation update --- .../components/awair/translations/ko.json | 29 ++++++++++++++ .../components/axis/translations/ko.json | 2 +- .../components/denonavr/translations/ko.json | 2 +- .../components/gogogate2/translations/ko.json | 2 +- .../components/harmony/translations/ko.json | 1 + .../huawei_lte/translations/ko.json | 1 + .../humidifier/translations/ko.json | 18 +++++++++ .../logi_circle/translations/ko.json | 4 +- .../components/mqtt/translations/ko.json | 32 +++++++++++++++ .../plum_lightpad/translations/ko.json | 18 +++++++++ .../components/point/translations/ko.json | 2 +- .../components/sms/translations/ko.json | 20 ++++++++++ .../squeezebox/translations/ko.json | 33 +++++++++++++++ .../components/toon/translations/ko.json | 14 +++++++ .../components/withings/translations/ko.json | 10 +++-- .../xiaomi_aqara/translations/ko.json | 40 +++++++++++++++++++ 16 files changed, 219 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/awair/translations/ko.json create mode 100644 homeassistant/components/humidifier/translations/ko.json create mode 100644 homeassistant/components/plum_lightpad/translations/ko.json create mode 100644 homeassistant/components/sms/translations/ko.json create mode 100644 homeassistant/components/squeezebox/translations/ko.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/ko.json diff --git a/homeassistant/components/awair/translations/ko.json b/homeassistant/components/awair/translations/ko.json new file mode 100644 index 00000000000..fcdf04e8b8a --- /dev/null +++ b/homeassistant/components/awair/translations/ko.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_devices": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "auth": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc54c \uc218 \uc5c6\ub294 Awair API \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "step": { + "reauth": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "email": "\uc774\uba54\uc77c" + }, + "description": "Awair \uac1c\ubc1c\uc790 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ub2e4\uc2dc \uc785\ub825\ud574\uc8fc\uc138\uc694." + }, + "user": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "email": "\uc774\uba54\uc77c" + }, + "description": "https://developer.getawair.com/onboard/login \uc5d0 Awair \uac1c\ubc1c\uc790 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ub4f1\ub85d\ud574\uc57c\ud569\ub2c8\ub2e4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ko.json b/homeassistant/components/axis/translations/ko.json index fc2b00a16bb..cb6920b4ae0 100644 --- a/homeassistant/components/axis/translations/ko.json +++ b/homeassistant/components/axis/translations/ko.json @@ -29,7 +29,7 @@ "step": { "configure_stream": { "data": { - "stream_profile": "\uc0ac\uc6a9\ud560 \uc2a4\ud2b8\ub9bc \ud504\ub85c\ud30c\uc77c\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694" + "stream_profile": "\uc0ac\uc6a9\ud560 \uc2a4\ud2b8\ub9bc \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694" }, "title": "Axis \uae30\uae30 \ube44\ub514\uc624 \uc2a4\ud2b8\ub9bc \uc635\uc158" } diff --git a/homeassistant/components/denonavr/translations/ko.json b/homeassistant/components/denonavr/translations/ko.json index f7e43da9ba0..b17dea154a3 100644 --- a/homeassistant/components/denonavr/translations/ko.json +++ b/homeassistant/components/denonavr/translations/ko.json @@ -21,7 +21,7 @@ "select_host": "\ub9ac\uc2dc\ubc84 IP" }, "description": "\ub9ac\uc2dc\ubc84 \uc5f0\uacb0\uc744 \ucd94\uac00\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694", - "title": "\uc5f0\uacb0\ud560 \ub9ac\uc2dc\ubc84\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + "title": "\uc5f0\uacb0\ud560 \ub9ac\uc2dc\ubc84\ub97c \uc120\ud0dd\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/gogogate2/translations/ko.json b/homeassistant/components/gogogate2/translations/ko.json index 55b32812bfa..8d680e327e9 100644 --- a/homeassistant/components/gogogate2/translations/ko.json +++ b/homeassistant/components/gogogate2/translations/ko.json @@ -14,7 +14,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "\uc544\ub798\uc5d0 \ud544\uc218 \uc815\ubcf4\ub97c \uc81c\uacf5\ud574\uc8fc\uc138\uc694.", + "description": "\uc544\ub798\uc5d0 \ud544\uc218 \uc815\ubcf4\ub97c \uc81c\uacf5\ud574\uc8fc\uc138\uc694. \ucc38\uace0: 'admin' \uc0ac\uc6a9\uc790\ub9cc \uc791\ub3d9\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.", "title": "GogoGate2 \uc124\uce58\ud558\uae30" } } diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json index 528f5e9cc7e..6e439f9c744 100644 --- a/homeassistant/components/harmony/translations/ko.json +++ b/homeassistant/components/harmony/translations/ko.json @@ -27,6 +27,7 @@ "init": { "data": { "activity": "\uc9c0\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc2e4\ud589\ud560 \uae30\ubcf8 \uc561\uc158.", + "activity_notify": "\uc561\uc158 \uc804\ud658 \uc2dc\uc791\uc2dc \ud604\uc7ac \uc561\uc158\uc744 \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.", "delay_secs": "\uba85\ub839 \uc804\uc1a1 \uc0ac\uc774\uc758 \uc9c0\uc5f0 \uc2dc\uac04." }, "description": "Harmony Hub \uc635\uc158 \uc870\uc815" diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 53a7c4fc822..79c08dfa66d 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -16,6 +16,7 @@ "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" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/humidifier/translations/ko.json b/homeassistant/components/humidifier/translations/ko.json new file mode 100644 index 00000000000..89548dc4e35 --- /dev/null +++ b/homeassistant/components/humidifier/translations/ko.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "{entity_name} \uc2b5\ub3c4 \uc124\uc815\ud558\uae30", + "set_mode": "{entity_name} \uc758 \uc6b4\uc804 \ubaa8\ub4dc \ubcc0\uacbd", + "toggle": "{entity_name} \ud1a0\uae00", + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" + } + }, + "state": { + "_": { + "off": "\uaebc\uc9d0", + "on": "\ucf1c\uc9d0" + } + }, + "title": "\uac00\uc2b5\uae30" +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/ko.json b/homeassistant/components/logi_circle/translations/ko.json index f3fe51e2d25..97c4d755184 100644 --- a/homeassistant/components/logi_circle/translations/ko.json +++ b/homeassistant/components/logi_circle/translations/ko.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_setup": "\ud558\ub098\uc758 Logi Circle \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "external_error": "\ub2e4\ub978 Flow \uc5d0\uc11c \uc608\uc678\uc0ac\ud56d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "external_setup": "Logi Circle \uc774 \ub2e4\ub978 Flow \uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "external_error": "\ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc608\uc678\uc0ac\ud56d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "external_setup": "Logi Circle \uc774 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "no_flows": "Logi Circle \uc744 \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Logi Circle \uc744 \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/logi_circle/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "create_entry": { diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index a337c05fe63..f713d564438 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -47,5 +47,37 @@ "button_short_release": "\"{subtype}\" \uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", "button_triple_press": "\"{subtype}\" \uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c" } + }, + "options": { + "error": { + "bad_birth": "Birth \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "bad_will": "Will \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "broker": { + "data": { + "broker": "\ube0c\ub85c\ucee4", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." + }, + "options": { + "data": { + "birth_payload": "Birth \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc", + "birth_qos": "Birth \uba54\uc2dc\uc9c0 QoS", + "birth_retain": "Birth \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", + "birth_topic": "Birth \uba54\uc2dc\uc9c0 \ud1a0\ud53d", + "discovery": "\uc7a5\uce58 \uac80\uc0c9 \ud65c\uc131\ud654", + "will_payload": "Will \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc", + "will_qos": "Will \uba54\uc2dc\uc9c0 QoS", + "will_retain": "Will \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", + "will_topic": "Will \uba54\uc2dc\uc9c0 \ud1a0\ud53d" + }, + "description": "MQTT \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/ko.json b/homeassistant/components/plum_lightpad/translations/ko.json new file mode 100644 index 00000000000..008177f1cec --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json index 417cbf949f5..0be554a417d 100644 --- a/homeassistant/components/point/translations/ko.json +++ b/homeassistant/components/point/translations/ko.json @@ -4,7 +4,7 @@ "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "external_setup": "Point \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", + "external_setup": "Point \uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "create_entry": { diff --git a/homeassistant/components/sms/translations/ko.json b/homeassistant/components/sms/translations/ko.json new file mode 100644 index 00000000000..13c043ef635 --- /dev/null +++ b/homeassistant/components/sms/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "device": "\uae30\uae30" + }, + "title": "\ubaa8\ub380\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/ko.json b/homeassistant/components/squeezebox/translations/ko.json new file mode 100644 index 00000000000..6ff17d6465c --- /dev/null +++ b/homeassistant/components/squeezebox/translations/ko.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_server_found": "\ubc1c\uacac\ub41c Logitech Media Server \uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_server_found": "\uc11c\ubc84\ub97c \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Logitech Squeezebox: {host}", + "step": { + "edit": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "\uc5f0\uacb0 \uc815\ubcf4 \ud3b8\uc9d1\ud558\uae30" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + }, + "title": "Logitech Media Server \uad6c\uc131\ud558\uae30" + } + } + }, + "title": "Logitech Squeezebox" +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json index 7bbde56dbe1..4004e5fdd57 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "\uc120\ud0dd\ub41c \uc57d\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "client_id": "\ud074\ub77c\uc774\uc5b8\ud2b8 ID \uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "client_secret": "\ud074\ub77c\uc774\uc5b8\ud2b8 \uc2dc\ud06c\ub9bf\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", "no_app": "Toon \uc744 \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Toon \uc744 \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/toon/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694.", "unknown_auth_fail": "\uc778\uc99d\ud558\ub294 \ub3d9\uc548 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." @@ -12,6 +16,13 @@ "display_exists": "\uc120\ud0dd\ub41c \ub514\uc2a4\ud50c\ub808\uc774\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { + "agreement": { + "data": { + "agreement": "\uc57d\uc815" + }, + "description": "\ucd94\uac00\ud560 \uc57d\uc815 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "\uc57d\uc815 \uc120\ud0dd\ud558\uae30" + }, "authenticate": { "data": { "password": "\ube44\ubc00\ubc88\ud638", @@ -27,6 +38,9 @@ }, "description": "\uc5f0\uacb0\ud560 Toon \ub514\uc2a4\ud50c\ub808\uc774\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", "title": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd\ud558\uae30" + }, + "pick_implementation": { + "title": "\uc778\uc99d \ub300\uc0c1 \uc0ac\uc6a9\uc790 \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json index 77531d8d313..74dacdd2cac 100644 --- a/homeassistant/components/withings/translations/ko.json +++ b/homeassistant/components/withings/translations/ko.json @@ -1,12 +1,16 @@ { "config": { "abort": { + "already_configured": "\ud504\ub85c\ud544\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "create_entry": { "default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "error": { + "profile_exists": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uace0\uc720\ud55c \ud504\ub85c\ud544 \uc774\ub984\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." + }, "flow_title": "Withings: {profile}", "step": { "pick_implementation": { @@ -14,14 +18,14 @@ }, "profile": { "data": { - "profile": "\ud504\ub85c\ud544" + "profile": "\ud504\ub85c\ud544 \uc774\ub984" }, - "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.", + "description": "\uace0\uc720\ud55c \ud504\ub85c\ud544 \uc774\ub984\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc77c\ubc18\uc801\uc73c\ub85c \uc774\uc804 \ub2e8\uacc4\uc5d0\uc11c \uc120\ud0dd\ud55c \ud504\ub85c\ud544\uc758 \uc774\ub984\uc785\ub2c8\ub2e4.", "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544." }, "reauth": { "description": "Withings \ub370\uc774\ud130\ub97c \uacc4\uc18d \uc218\uc2e0\ud558\ub824\uba74 \"{profile}\" \ud504\ub85c\ud544\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", - "title": "{profile} \uc7ac\uc778\uc99d" + "title": "\ud504\ub85c\ud544 \uc7ac\uc778\uc99d\ud558\uae30" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json new file mode 100644 index 00000000000..38f58148e4c --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/ko.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "not_xiaomi_aqara": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc544\ub2d9\ub2c8\ub2e4. \ubc1c\uacac\ub41c \uae30\uae30\uac00 \uc54c\ub824\uc9c4 \uac8c\uc774\ud2b8\uc6e8\uc774\uc640 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + }, + "error": { + "discovery_error": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ubc1c\uacac\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. HomeAssistant \ub97c \uc778\ud130\ud398\uc774\uc2a4\ub85c \uc0ac\uc6a9\ud558\ub294 \uae30\uae30\uc758 IP \ub85c \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694.", + "invalid_interface": "\ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_found_error": "Zeroconf \uc5d0\uc11c \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc5b4 \ud544\uc694\ud55c \uc815\ubcf4\ub97c \uc5bb\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. HomeAssistant \ub97c \uc778\ud130\ud398\uc774\uc2a4\ub85c \uc0ac\uc6a9\ud558\ub294 \uae30\uae30\uc758 IP \ub85c \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694." + }, + "flow_title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774: {name}", + "step": { + "select": { + "data": { + "select_ip": "\uac8c\uc774\ud2b8\uc6e8\uc774 IP" + }, + "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc5f0\uacb0\uc744 \ucd94\uac00\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694", + "title": "\uc5f0\uacb0\ud560 Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd\ud558\uae30" + }, + "settings": { + "data": { + "key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4", + "name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984" + }, + "description": "\ud0a4(\ube44\ubc00\ubc88\ud638)\ub97c \uc5bb\uc740 \ubc29\ubc95\uc740 \ub2e4\uc74c\uc758 \uc548\ub0b4\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \ud0a4\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc73c\uba74 \uc13c\uc11c\uc5d0\ub9cc \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774 \ucd94\uac00 \uc124\uc815\ud558\uae30" + }, + "user": { + "data": { + "interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4" + }, + "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4", + "title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774" + } + } + } +} \ No newline at end of file From a63a11a11ab6a4f8b11037f174416a994279dbc0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Jun 2020 19:48:27 -0500 Subject: [PATCH 042/428] Ensure all async_track_state_change_event callbacks run if one throws (#37179) --- homeassistant/helpers/event.py | 10 +++++++++- tests/helpers/test_event.py | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 46c36205cee..a5f433b0e23 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,6 +1,7 @@ """Helpers for listening to events.""" from datetime import datetime, timedelta import functools as ft +import logging from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Union import attr @@ -24,6 +25,8 @@ from homeassistant.util.async_ import run_callback_threadsafe TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" +_LOGGER = logging.getLogger(__name__) + # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name @@ -146,7 +149,12 @@ def async_track_state_change_event( return for action in entity_callbacks[entity_id]: - hass.async_run_job(action, event) + try: + hass.async_run_job(action, event) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error while processing state changed for %s", entity_id + ) hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen( EVENT_STATE_CHANGED, _async_state_change_dispatcher diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 95a093d59ab..2d5ee9a9a73 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -183,12 +183,19 @@ async def test_async_track_state_change_event(hass): multiple_entity_id_tracker.append((old_state, new_state)) + @ha.callback + def callback_that_throws(event): + raise ValueError + unsub_single = async_track_state_change_event( hass, ["light.Bowl"], single_run_callback ) unsub_multi = async_track_state_change_event( hass, ["light.Bowl", "switch.kitchen"], multiple_run_callback ) + unsub_throws = async_track_state_change_event( + hass, ["light.Bowl", "switch.kitchen"], callback_that_throws + ) # Adding state to state machine hass.states.async_set("light.Bowl", "on") @@ -247,6 +254,7 @@ async def test_async_track_state_change_event(hass): assert len(multiple_entity_id_tracker) == 7 unsub_multi() + unsub_throws() async def test_track_template(hass): @@ -429,6 +437,7 @@ async def test_track_same_state_simple_trigger_check_funct(hass): # Adding state to state machine hass.states.async_set("light.Bowl", "on") await hass.async_block_till_done() + await hass.async_block_till_done() assert len(callback_runs) == 0 assert check_func[-1][2].state == "on" assert check_func[-1][0] == "light.bowl" From 464f17f1827949b04510ed4b1ccf1968624c4c41 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 28 Jun 2020 06:54:27 +0200 Subject: [PATCH 043/428] Fixup rfxtrx tests to at least run (#37186) --- tests/components/rfxtrx/test_cover.py | 93 ++------------ tests/components/rfxtrx/test_init.py | 23 ++-- tests/components/rfxtrx/test_light.py | 118 +++--------------- tests/components/rfxtrx/test_sensor.py | 65 +++------- tests/components/rfxtrx/test_switch.py | 164 +++++-------------------- 5 files changed, 91 insertions(+), 372 deletions(-) diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index 9ecbcda3409..2c45189bf39 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -17,15 +17,15 @@ class TestCoverRfxtrx(unittest.TestCase): def setUp(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - mock_component("rfxtrx") + mock_component(self.hass, "rfxtrx") self.addCleanup(self.tear_down_cleanup) def tear_down_cleanup(self): """Stop everything that was started.""" - rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS = [] - rfxtrx_core.RFX_DEVICES = {} - if rfxtrx_core.RFXOBJECT: - rfxtrx_core.RFXOBJECT.close_connection() + rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS.clear() + rfxtrx_core.RFX_DEVICES.clear() + if rfxtrx_core.DATA_RFXOBJECT in self.hass.data: + self.hass.data[rfxtrx_core.DATA_RFXOBJECT].close_connection() self.hass.stop() def test_valid_config(self): @@ -40,90 +40,13 @@ class TestCoverRfxtrx(unittest.TestCase): "devices": { "0b1100cd0213c7f210010f51": { "name": "Test", - rfxtrx_core.ATTR_FIREEVENT: True, + rfxtrx_core.ATTR_FIRE_EVENT: True, } }, } }, ) - def test_invalid_config_capital_letters(self): - """Test configuration.""" - assert not setup_component( - self.hass, - "cover", - { - "cover": { - "platform": "rfxtrx", - "automatic_add": True, - "devices": { - "2FF7f216": { - "name": "Test", - "packetid": "0b1100cd0213c7f210010f51", - "signal_repetitions": 3, - } - }, - } - }, - ) - - def test_invalid_config_extra_key(self): - """Test configuration.""" - assert not setup_component( - self.hass, - "cover", - { - "cover": { - "platform": "rfxtrx", - "automatic_add": True, - "invalid_key": "afda", - "devices": { - "213c7f216": { - "name": "Test", - "packetid": "0b1100cd0213c7f210010f51", - rfxtrx_core.ATTR_FIREEVENT: True, - } - }, - } - }, - ) - - def test_invalid_config_capital_packetid(self): - """Test configuration.""" - assert not setup_component( - self.hass, - "cover", - { - "cover": { - "platform": "rfxtrx", - "automatic_add": True, - "devices": { - "213c7f216": { - "name": "Test", - "packetid": "AA1100cd0213c7f210010f51", - rfxtrx_core.ATTR_FIREEVENT: True, - } - }, - } - }, - ) - - def test_invalid_config_missing_packetid(self): - """Test configuration.""" - assert not setup_component( - self.hass, - "cover", - { - "cover": { - "platform": "rfxtrx", - "automatic_add": True, - "devices": { - "213c7f216": {"name": "Test", rfxtrx_core.ATTR_FIREEVENT: True} - }, - } - }, - ) - def test_default_config(self): """Test with 0 cover.""" assert setup_component( @@ -143,8 +66,8 @@ class TestCoverRfxtrx(unittest.TestCase): } }, ) - - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( + self.hass.block_till_done() + self.hass.data[rfxtrx_core.DATA_RFXOBJECT] = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 669ec24efc3..ca856f8172b 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -1,5 +1,6 @@ """The tests for the Rfxtrx component.""" # pylint: disable=protected-access +import time import unittest import pytest @@ -22,10 +23,10 @@ class TestRFXTRX(unittest.TestCase): def tear_down_cleanup(self): """Stop everything that was started.""" - rfxtrx.RECEIVED_EVT_SUBSCRIBERS = [] - rfxtrx.RFX_DEVICES = {} - if rfxtrx.RFXOBJECT: - rfxtrx.RFXOBJECT.close_connection() + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.clear() + rfxtrx.RFX_DEVICES.clear() + if rfxtrx.DATA_RFXOBJECT in self.hass.data: + self.hass.data[rfxtrx.DATA_RFXOBJECT].close_connection() self.hass.stop() def test_default_config(self): @@ -48,7 +49,9 @@ class TestRFXTRX(unittest.TestCase): {"sensor": {"platform": "rfxtrx", "automatic_add": True, "devices": {}}}, ) - assert len(rfxtrx.RFXOBJECT.sensors()) == 2 + time.sleep(1) # Dummy startup is slow + + assert len(self.hass.data[rfxtrx.DATA_RFXOBJECT].sensors()) == 2 def test_valid_config(self): """Test configuration.""" @@ -118,7 +121,7 @@ class TestRFXTRX(unittest.TestCase): "devices": { "0b1100cd0213c7f210010f51": { "name": "Test", - rfxtrx.ATTR_FIREEVENT: True, + rfxtrx.ATTR_FIRE_EVENT: True, } }, } @@ -134,8 +137,8 @@ class TestRFXTRX(unittest.TestCase): self.hass.bus.listen(rfxtrx.EVENT_BUTTON_PRESSED, record_event) self.hass.block_till_done() - - entity = rfxtrx.RFX_DEVICES["213c7f216"] + entity = rfxtrx.RFX_DEVICES["213c7f2_16"] + entity.update_state(False, 0) assert "Test" == entity.name assert "off" == entity.state assert entity.should_fire_event @@ -176,7 +179,7 @@ class TestRFXTRX(unittest.TestCase): "devices": { "0a520802060100ff0e0269": { "name": "Test", - rfxtrx.ATTR_FIREEVENT: True, + rfxtrx.ATTR_FIRE_EVENT: True, } }, } @@ -198,4 +201,4 @@ class TestRFXTRX(unittest.TestCase): self.hass.block_till_done() assert 1 == len(calls) - assert calls[0].data == {"entity_id": "sensor.test"} + assert calls[0].data == {"entity_id": "sensor.test_temperature"} diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index 1a8ee94d9a4..c08afbbecce 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -1,7 +1,6 @@ """The tests for the Rfxtrx light platform.""" import unittest -import RFXtrx as rfxtrxmod import pytest from homeassistant.components import rfxtrx as rfxtrx_core @@ -22,10 +21,10 @@ class TestLightRfxtrx(unittest.TestCase): def tear_down_cleanup(self): """Stop everything that was started.""" - rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS = [] - rfxtrx_core.RFX_DEVICES = {} - if rfxtrx_core.RFXOBJECT: - rfxtrx_core.RFXOBJECT.close_connection() + rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS.clear() + rfxtrx_core.RFX_DEVICES.clear() + if rfxtrx_core.DATA_RFXOBJECT in self.hass.data: + self.hass.data[rfxtrx_core.DATA_RFXOBJECT].close_connection() self.hass.stop() def test_valid_config(self): @@ -40,7 +39,7 @@ class TestLightRfxtrx(unittest.TestCase): "devices": { "0b1100cd0213c7f210010f51": { "name": "Test", - rfxtrx_core.ATTR_FIREEVENT: True, + rfxtrx_core.ATTR_FIRE_EVENT: True, } }, } @@ -55,9 +54,8 @@ class TestLightRfxtrx(unittest.TestCase): "platform": "rfxtrx", "automatic_add": True, "devices": { - "213c7f216": { + "0b1100cd0213c7f210010f51": { "name": "Test", - "packetid": "0b1100cd0213c7f210010f51", "signal_repetitions": 3, } }, @@ -65,27 +63,6 @@ class TestLightRfxtrx(unittest.TestCase): }, ) - def test_invalid_config(self): - """Test configuration.""" - assert not setup_component( - self.hass, - "light", - { - "light": { - "platform": "rfxtrx", - "automatic_add": True, - "invalid_key": "afda", - "devices": { - "213c7f216": { - "name": "Test", - "packetid": "0b1100cd0213c7f210010f51", - rfxtrx_core.ATTR_FIREEVENT: True, - } - }, - } - }, - ) - def test_default_config(self): """Test with 0 switches.""" assert setup_component( @@ -93,59 +70,6 @@ class TestLightRfxtrx(unittest.TestCase): ) assert 0 == len(rfxtrx_core.RFX_DEVICES) - def test_old_config(self): - """Test with 1 light.""" - assert setup_component( - self.hass, - "light", - { - "light": { - "platform": "rfxtrx", - "devices": { - "123efab1": { - "name": "Test", - "packetid": "0b1100cd0213c7f210010f51", - } - }, - } - }, - ) - - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( - "", transport_protocol=rfxtrxmod.DummyTransport - ) - - assert 1 == len(rfxtrx_core.RFX_DEVICES) - entity = rfxtrx_core.RFX_DEVICES["213c7f216"] - assert "Test" == entity.name - assert "off" == entity.state - assert entity.assumed_state - assert entity.signal_repetitions == 1 - assert not entity.should_fire_event - assert not entity.should_poll - - assert not entity.is_on - - entity.turn_on() - assert entity.is_on - assert entity.brightness == 255 - - entity.turn_off() - assert not entity.is_on - assert entity.brightness == 0 - - entity.turn_on(brightness=100) - assert entity.is_on - assert entity.brightness == 100 - - entity.turn_on(brightness=10) - assert entity.is_on - assert entity.brightness == 10 - - entity.turn_on(brightness=255) - assert entity.is_on - assert entity.brightness == 255 - def test_one_light(self): """Test with 1 light.""" assert setup_component( @@ -161,12 +85,13 @@ class TestLightRfxtrx(unittest.TestCase): import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( + self.hass.data[rfxtrx_core.DATA_RFXOBJECT] = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) assert 1 == len(rfxtrx_core.RFX_DEVICES) - entity = rfxtrx_core.RFX_DEVICES["213c7f216"] + entity = rfxtrx_core.RFX_DEVICES["213c7f2_16"] + entity.hass = self.hass assert "Test" == entity.name assert "off" == entity.state assert entity.assumed_state @@ -197,30 +122,23 @@ class TestLightRfxtrx(unittest.TestCase): assert entity.brightness == 255 entity.turn_off() - entity_id = rfxtrx_core.RFX_DEVICES["213c7f216"].entity_id - entity_hass = self.hass.states.get(entity_id) - assert "Test" == entity_hass.name - assert "off" == entity_hass.state + assert "Test" == entity.name + assert "off" == entity.state entity.turn_on() - entity_hass = self.hass.states.get(entity_id) - assert "on" == entity_hass.state + assert "on" == entity.state entity.turn_off() - entity_hass = self.hass.states.get(entity_id) - assert "off" == entity_hass.state + assert "off" == entity.state entity.turn_on(brightness=100) - entity_hass = self.hass.states.get(entity_id) - assert "on" == entity_hass.state + assert "on" == entity.state entity.turn_on(brightness=10) - entity_hass = self.hass.states.get(entity_id) - assert "on" == entity_hass.state + assert "on" == entity.state entity.turn_on(brightness=255) - entity_hass = self.hass.states.get(entity_id) - assert "on" == entity_hass.state + assert "on" == entity.state def test_several_lights(self): """Test with 3 lights.""" @@ -272,7 +190,7 @@ class TestLightRfxtrx(unittest.TestCase): event.data = bytearray(b"\x0b\x11\x00\x9e\x00\xe6\x11b\x02\x02\x00p") rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) - entity = rfxtrx_core.RFX_DEVICES["0e611622"] + entity = rfxtrx_core.RFX_DEVICES["0e61162_2"] assert 1 == len(rfxtrx_core.RFX_DEVICES) assert "" == entity.__str__() @@ -288,7 +206,7 @@ class TestLightRfxtrx(unittest.TestCase): ) rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) - entity = rfxtrx_core.RFX_DEVICES["118cdea2"] + entity = rfxtrx_core.RFX_DEVICES["118cdea_2"] assert 2 == len(rfxtrx_core.RFX_DEVICES) assert "" == entity.__str__() diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index fa713a024cf..43f27c9decf 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -22,10 +22,10 @@ class TestSensorRfxtrx(unittest.TestCase): def tear_down_cleanup(self): """Stop everything that was started.""" - rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS = [] - rfxtrx_core.RFX_DEVICES = {} - if rfxtrx_core.RFXOBJECT: - rfxtrx_core.RFXOBJECT.close_connection() + rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS.clear() + rfxtrx_core.RFX_DEVICES.clear() + if rfxtrx_core.DATA_RFXOBJECT in self.hass.data: + self.hass.data[rfxtrx_core.DATA_RFXOBJECT].close_connection() self.hass.stop() def test_default_config(self): @@ -35,31 +35,6 @@ class TestSensorRfxtrx(unittest.TestCase): ) assert 0 == len(rfxtrx_core.RFX_DEVICES) - def test_old_config_sensor(self): - """Test with 1 sensor.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": { - "platform": "rfxtrx", - "devices": { - "sensor_0502": { - "name": "Test", - "packetid": "0a52080705020095220269", - "data_type": "Temperature", - } - }, - } - }, - ) - - assert 1 == len(rfxtrx_core.RFX_DEVICES) - entity = rfxtrx_core.RFX_DEVICES["sensor_0502"]["Temperature"] - assert "Test" == entity.name - assert TEMP_CELSIUS == entity.unit_of_measurement - assert entity.state is None - def test_one_sensor(self): """Test with 1 sensor.""" assert setup_component( @@ -79,8 +54,8 @@ class TestSensorRfxtrx(unittest.TestCase): ) assert 1 == len(rfxtrx_core.RFX_DEVICES) - entity = rfxtrx_core.RFX_DEVICES["sensor_0502"]["Temperature"] - assert "Test" == entity.name + entity = rfxtrx_core.RFX_DEVICES["sensor_05_02"]["Temperature"] + assert "Test Temperature" == entity.name assert TEMP_CELSIUS == entity.unit_of_measurement assert entity.state is None @@ -98,16 +73,11 @@ class TestSensorRfxtrx(unittest.TestCase): ) assert 1 == len(rfxtrx_core.RFX_DEVICES) - entity = rfxtrx_core.RFX_DEVICES["sensor_0502"]["Temperature"] - assert "Test" == entity.name + entity = rfxtrx_core.RFX_DEVICES["sensor_05_02"]["Temperature"] + assert "Test Temperature" == entity.name assert TEMP_CELSIUS == entity.unit_of_measurement assert entity.state is None - entity_id = rfxtrx_core.RFX_DEVICES["sensor_0502"]["Temperature"].entity_id - entity = self.hass.states.get(entity_id) - assert "Test" == entity.name - assert "unknown" == entity.state - def test_several_sensors(self): """Test with 3 sensors.""" assert setup_component( @@ -133,7 +103,7 @@ class TestSensorRfxtrx(unittest.TestCase): assert 2 == len(rfxtrx_core.RFX_DEVICES) device_num = 0 for id in rfxtrx_core.RFX_DEVICES: - if id == "sensor_0601": + if id == "sensor_06_01": device_num = device_num + 1 assert len(rfxtrx_core.RFX_DEVICES[id]) == 2 _entity_temp = rfxtrx_core.RFX_DEVICES[id]["Temperature"] @@ -143,7 +113,7 @@ class TestSensorRfxtrx(unittest.TestCase): assert _entity_hum.state is None assert TEMP_CELSIUS == _entity_temp.unit_of_measurement assert "Bath" == _entity_temp.__str__() - elif id == "sensor_0502": + elif id == "sensor_05_02": device_num = device_num + 1 entity = rfxtrx_core.RFX_DEVICES[id]["Temperature"] assert entity.state is None @@ -159,12 +129,13 @@ class TestSensorRfxtrx(unittest.TestCase): "sensor", {"sensor": {"platform": "rfxtrx", "automatic_add": True, "devices": {}}}, ) + self.hass.block_till_done() event = rfxtrx_core.get_rfx_object("0a520801070100b81b0279") event.data = bytearray(b"\nR\x08\x01\x07\x01\x00\xb8\x1b\x02y") rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) - entity = rfxtrx_core.RFX_DEVICES["sensor_0701"]["Temperature"] + entity = rfxtrx_core.RFX_DEVICES["sensor_07_01"]["Temperature"] assert 1 == len(rfxtrx_core.RFX_DEVICES) assert { "Humidity status": "normal", @@ -182,7 +153,7 @@ class TestSensorRfxtrx(unittest.TestCase): event = rfxtrx_core.get_rfx_object("0a52080405020095240279") event.data = bytearray(b"\nR\x08\x04\x05\x02\x00\x95$\x02y") rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) - entity = rfxtrx_core.RFX_DEVICES["sensor_0502"]["Temperature"] + entity = rfxtrx_core.RFX_DEVICES["sensor_05_02"]["Temperature"] assert 2 == len(rfxtrx_core.RFX_DEVICES) assert { "Humidity status": "normal", @@ -197,7 +168,7 @@ class TestSensorRfxtrx(unittest.TestCase): event = rfxtrx_core.get_rfx_object("0a52085e070100b31b0279") event.data = bytearray(b"\nR\x08^\x07\x01\x00\xb3\x1b\x02y") rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) - entity = rfxtrx_core.RFX_DEVICES["sensor_0701"]["Temperature"] + entity = rfxtrx_core.RFX_DEVICES["sensor_07_01"]["Temperature"] assert 2 == len(rfxtrx_core.RFX_DEVICES) assert { "Humidity status": "normal", @@ -267,7 +238,7 @@ class TestSensorRfxtrx(unittest.TestCase): assert 2 == len(rfxtrx_core.RFX_DEVICES) device_num = 0 for id in rfxtrx_core.RFX_DEVICES: - if id == "sensor_0601": + if id == "sensor_06_01": device_num = device_num + 1 assert len(rfxtrx_core.RFX_DEVICES[id]) == 2 _entity_temp = rfxtrx_core.RFX_DEVICES[id]["Temperature"] @@ -277,7 +248,7 @@ class TestSensorRfxtrx(unittest.TestCase): assert _entity_temp.state is None assert TEMP_CELSIUS == _entity_temp.unit_of_measurement assert "Bath" == _entity_temp.__str__() - elif id == "sensor_0502": + elif id == "sensor_05_02": device_num = device_num + 1 entity = rfxtrx_core.RFX_DEVICES[id]["Temperature"] assert entity.state is None @@ -299,7 +270,7 @@ class TestSensorRfxtrx(unittest.TestCase): device_num = 0 for id in rfxtrx_core.RFX_DEVICES: - if id == "sensor_0601": + if id == "sensor_06_01": device_num = device_num + 1 assert len(rfxtrx_core.RFX_DEVICES[id]) == 2 _entity_temp = rfxtrx_core.RFX_DEVICES[id]["Temperature"] @@ -327,7 +298,7 @@ class TestSensorRfxtrx(unittest.TestCase): "Rssi numeric": 6, } == _entity_temp.device_state_attributes assert "Bath" == _entity_temp.__str__() - elif id == "sensor_0502": + elif id == "sensor_05_02": device_num = device_num + 1 entity = rfxtrx_core.RFX_DEVICES[id]["Temperature"] assert TEMP_CELSIUS == entity.unit_of_measurement diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index cc1a6008fa8..aadcf8beb07 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components import rfxtrx as rfxtrx_core from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, mock_component +from tests.common import assert_setup_component, get_test_home_assistant, mock_component @pytest.mark.skipif("os.environ.get('RFXTRX') != 'RUN'") @@ -22,10 +22,10 @@ class TestSwitchRfxtrx(unittest.TestCase): def tear_down_cleanup(self): """Stop everything that was started.""" - rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS = [] - rfxtrx_core.RFX_DEVICES = {} - if rfxtrx_core.RFXOBJECT: - rfxtrx_core.RFXOBJECT.close_connection() + rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS.clear() + rfxtrx_core.RFX_DEVICES.clear() + if rfxtrx_core.DATA_RFXOBJECT in self.hass.data: + self.hass.data[rfxtrx_core.DATA_RFXOBJECT].close_connection() self.hass.stop() def test_valid_config(self): @@ -40,7 +40,7 @@ class TestSwitchRfxtrx(unittest.TestCase): "devices": { "0b1100cd0213c7f210010f51": { "name": "Test", - rfxtrx_core.ATTR_FIREEVENT: True, + rfxtrx_core.ATTR_FIRE_EVENT: True, } }, } @@ -59,27 +59,7 @@ class TestSwitchRfxtrx(unittest.TestCase): "devices": { 710000141010170: { "name": "Test", - rfxtrx_core.ATTR_FIREEVENT: True, - } - }, - } - }, - ) - - def test_invalid_config1(self): - """Test invalid configuration.""" - assert not setup_component( - self.hass, - "switch", - { - "switch": { - "platform": "rfxtrx", - "automatic_add": True, - "devices": { - "2FF7f216": { - "name": "Test", - "packetid": "0b1100cd0213c7f210010f51", - "signal_repetitions": 3, + rfxtrx_core.ATTR_FIRE_EVENT: True, } }, } @@ -88,60 +68,24 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_invalid_config2(self): """Test invalid configuration.""" - assert not setup_component( - self.hass, - "switch", - { - "switch": { - "platform": "rfxtrx", - "automatic_add": True, - "invalid_key": "afda", - "devices": { - "213c7f216": { - "name": "Test", - "packetid": "0b1100cd0213c7f210010f51", - rfxtrx_core.ATTR_FIREEVENT: True, - } - }, - } - }, - ) - - def test_invalid_config3(self): - """Test invalid configuration.""" - assert not setup_component( - self.hass, - "switch", - { - "switch": { - "platform": "rfxtrx", - "automatic_add": True, - "devices": { - "213c7f216": { - "name": "Test", - "packetid": "AA1100cd0213c7f210010f51", - rfxtrx_core.ATTR_FIREEVENT: True, - } - }, - } - }, - ) - - def test_invalid_config4(self): - """Test configuration.""" - assert not setup_component( - self.hass, - "switch", - { - "switch": { - "platform": "rfxtrx", - "automatic_add": True, - "devices": { - "213c7f216": {"name": "Test", rfxtrx_core.ATTR_FIREEVENT: True} - }, - } - }, - ) + with assert_setup_component(0): + setup_component( + self.hass, + "switch", + { + "switch": { + "platform": "rfxtrx", + "automatic_add": True, + "invalid_key": "afda", + "devices": { + "0b1100cd0213c7f210010f51": { + "name": "Test", + rfxtrx_core.ATTR_FIRE_EVENT: True, + } + }, + } + }, + ) def test_default_config(self): """Test with 0 switches.""" @@ -150,43 +94,6 @@ class TestSwitchRfxtrx(unittest.TestCase): ) assert 0 == len(rfxtrx_core.RFX_DEVICES) - def test_old_config(self): - """Test with 1 switch.""" - assert setup_component( - self.hass, - "switch", - { - "switch": { - "platform": "rfxtrx", - "devices": { - "123efab1": { - "name": "Test", - "packetid": "0b1100cd0213c7f210010f51", - } - }, - } - }, - ) - - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( - "", transport_protocol=rfxtrxmod.DummyTransport - ) - - assert 1 == len(rfxtrx_core.RFX_DEVICES) - entity = rfxtrx_core.RFX_DEVICES["213c7f216"] - assert "Test" == entity.name - assert "off" == entity.state - assert entity.assumed_state - assert entity.signal_repetitions == 1 - assert not entity.should_fire_event - assert not entity.should_poll - - assert not entity.is_on - entity.turn_on() - assert entity.is_on - entity.turn_off() - assert not entity.is_on - def test_one_switch(self): """Test with 1 switch.""" assert setup_component( @@ -200,12 +107,13 @@ class TestSwitchRfxtrx(unittest.TestCase): }, ) - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( + self.hass.data[rfxtrx_core.DATA_RFXOBJECT] = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) assert 1 == len(rfxtrx_core.RFX_DEVICES) - entity = rfxtrx_core.RFX_DEVICES["213c7f216"] + entity = rfxtrx_core.RFX_DEVICES["213c7f2_16"] + entity.hass = self.hass assert "Test" == entity.name assert "off" == entity.state assert entity.assumed_state @@ -219,16 +127,12 @@ class TestSwitchRfxtrx(unittest.TestCase): entity.turn_off() assert not entity.is_on - entity_id = rfxtrx_core.RFX_DEVICES["213c7f216"].entity_id - entity_hass = self.hass.states.get(entity_id) - assert "Test" == entity_hass.name - assert "off" == entity_hass.state + assert "Test" == entity.name + assert "off" == entity.state entity.turn_on() - entity_hass = self.hass.states.get(entity_id) - assert "on" == entity_hass.state + assert "on" == entity.state entity.turn_off() - entity_hass = self.hass.states.get(entity_id) - assert "off" == entity_hass.state + assert "off" == entity.state def test_several_switches(self): """Test with 3 switches.""" @@ -282,7 +186,7 @@ class TestSwitchRfxtrx(unittest.TestCase): ) rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) - entity = rfxtrx_core.RFX_DEVICES["118cdea2"] + entity = rfxtrx_core.RFX_DEVICES["118cdea_2"] assert 1 == len(rfxtrx_core.RFX_DEVICES) assert "" == entity.__str__() @@ -295,7 +199,7 @@ class TestSwitchRfxtrx(unittest.TestCase): ) rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event) - entity = rfxtrx_core.RFX_DEVICES["118cdeb2"] + entity = rfxtrx_core.RFX_DEVICES["118cdeb_2"] assert 2 == len(rfxtrx_core.RFX_DEVICES) assert "" == entity.__str__() From 10f296ba178a5f05d013854bdc18baf8e16fb806 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 27 Jun 2020 22:54:50 -0600 Subject: [PATCH 044/428] Fix bug where Tile session would expire (#37185) --- homeassistant/components/tile/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index ec9e6bb0f45..4f6411ed368 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta from pytile import async_login -from pytile.errors import TileError +from pytile.errors import SessionExpiredError, TileError from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback @@ -44,6 +44,9 @@ async def async_setup_entry(hass, config_entry): """Get new data from the API.""" try: return await client.tiles.all() + except SessionExpiredError: + LOGGER.info("Tile session expired; creating a new one") + await client.async_init() except TileError as err: raise UpdateFailed(f"Error while retrieving data: {err}") From 201dab93ff3f1869a7cb4069c88329e3a4ebe994 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 28 Jun 2020 06:59:42 +0200 Subject: [PATCH 045/428] Attempt to set unique id of rfxtrx device (#37159) --- homeassistant/components/rfxtrx/__init__.py | 6 ++++++ homeassistant/components/rfxtrx/binary_sensor.py | 6 ++++++ homeassistant/components/rfxtrx/sensor.py | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index fb57359829c..6c631cad029 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -378,6 +378,7 @@ class RfxtrxDevice(Entity): self._state = datas[ATTR_STATE] self._should_fire_event = datas[ATTR_FIRE_EVENT] self._brightness = 0 + self._unique_id = f"{slugify(self._event.device.type_string.lower())}_{slugify(self._event.device.id_string.lower())}" self.added_to_hass = False async def async_added_to_hass(self): @@ -409,6 +410,11 @@ class RfxtrxDevice(Entity): """Return true if unable to access real state of entity.""" return True + @property + def unique_id(self): + """Return unique identifier of remote device.""" + return self._unique_id + def turn_off(self, **kwargs): """Turn the device off.""" self._send_command("turn_off") diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 5e610128ea6..6426f824320 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -197,6 +197,7 @@ class RfxtrxBinarySensor(BinarySensorEntity): self._data_bits = data_bits self._cmd_on = cmd_on self._cmd_off = cmd_off + self._unique_id = f"{slugify(self.event.device.type_string.lower())}_{slugify(self.event.device.id_string.lower())}" if data_bits is not None: self._masked_id = get_pt2262_deviceid( @@ -255,6 +256,11 @@ class RfxtrxBinarySensor(BinarySensorEntity): """Return true if the sensor state is True.""" return self._state + @property + def unique_id(self): + """Return unique identifier of remote device.""" + return self._unique_id + def apply_cmd(self, cmd): """Apply a command for updating the state.""" if cmd == self.cmd_on: diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 759268140fc..1b053156fd8 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -127,6 +127,7 @@ class RfxtrxSensor(Entity): self.should_fire_event = should_fire_event self.data_type = data_type self._unit_of_measurement = DATA_TYPES.get(data_type, "") + self._unique_id = f"{slugify(self.event.device.type_string.lower())}_{slugify(self.event.device.id_string.lower())}_{slugify(self.data_type)}" def __str__(self): """Return the name of the sensor.""" @@ -155,3 +156,8 @@ class RfxtrxSensor(Entity): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement + + @property + def unique_id(self): + """Return unique identifier of remote device.""" + return self._unique_id From b0df223f5a96e98b09679592cc27fe52f348b2e1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 27 Jun 2020 23:16:42 -0600 Subject: [PATCH 046/428] Bump aioguardian (#37188) * Bump aioguardian * Fix tests --- homeassistant/components/guardian/__init__.py | 22 +++++++++---------- .../components/guardian/config_flow.py | 2 +- .../components/guardian/manifest.json | 2 +- homeassistant/components/guardian/switch.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/guardian/conftest.py | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 8ccf69c7077..7fc12bece26 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta from aioguardian import Client -from aioguardian.commands.device import ( +from aioguardian.commands.system import ( DEFAULT_FIRMWARE_UPGRADE_FILENAME, DEFAULT_FIRMWARE_UPGRADE_PORT, DEFAULT_FIRMWARE_UPGRADE_URL, @@ -102,7 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Disable the device's onboard access point.""" try: async with guardian.client: - await guardian.client.device.wifi_disable_ap() + await guardian.client.wifi.disable_ap() except GuardianError as err: LOGGER.error("Error during service call: %s", err) return @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Enable the device's onboard access point.""" try: async with guardian.client: - await guardian.client.device.wifi_enable_ap() + await guardian.client.wifi.enable_ap() except GuardianError as err: LOGGER.error("Error during service call: %s", err) return @@ -122,7 +122,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Reboot the device.""" try: async with guardian.client: - await guardian.client.device.reboot() + await guardian.client.system.reboot() except GuardianError as err: LOGGER.error("Error during service call: %s", err) return @@ -132,7 +132,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Fully reset system motor diagnostics.""" try: async with guardian.client: - await guardian.client.valve.valve_reset() + await guardian.client.valve.reset() except GuardianError as err: LOGGER.error("Error during service call: %s", err) return @@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Upgrade the device firmware.""" try: async with guardian.client: - await guardian.client.device.upgrade_firmware( + await guardian.client.system.upgrade_firmware( url=call.data[CONF_URL], port=call.data[CONF_PORT], filename=call.data[CONF_FILENAME], @@ -191,12 +191,12 @@ class Guardian: self.uid = entry.data[CONF_UID] self._api_coros = { - DATA_DIAGNOSTICS: self.client.device.diagnostics, + DATA_DIAGNOSTICS: self.client.system.diagnostics, DATA_PAIR_DUMP: self.client.sensor.pair_dump, - DATA_PING: self.client.device.ping, - DATA_SENSOR_STATUS: self.client.sensor.sensor_status, - DATA_VALVE_STATUS: self.client.valve.valve_status, - DATA_WIFI_STATUS: self.client.device.wifi_status, + DATA_PING: self.client.system.ping, + DATA_SENSOR_STATUS: self.client.system.onboard_sensor_status, + DATA_VALVE_STATUS: self.client.valve.status, + DATA_WIFI_STATUS: self.client.wifi.status, } self._api_category_count = { diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index 769344e3b01..71ec271753e 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -34,7 +34,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ async with Client(data[CONF_IP_ADDRESS]) as client: - ping_data = await client.device.ping() + ping_data = await client.system.ping() return { CONF_UID: ping_data["data"]["uid"], diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index a3e2d9e66ee..72a11d15cfb 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", "requirements": [ - "aioguardian==0.2.3" + "aioguardian==1.0.0" ], "ssdp": [], "zeroconf": [ diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 39945f4da4e..f8af11768d2 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -64,7 +64,7 @@ class GuardianSwitch(GuardianEntity, SwitchEntity): """Turn the valve off (closed).""" try: async with self._guardian.client: - await self._guardian.client.valve.valve_close() + await self._guardian.client.valve.close() except GuardianError as err: LOGGER.error("Error while closing the valve: %s", err) return @@ -76,7 +76,7 @@ class GuardianSwitch(GuardianEntity, SwitchEntity): """Turn the valve on (open).""" try: async with self._guardian.client: - await self._guardian.client.valve.valve_open() + await self._guardian.client.valve.open() except GuardianError as err: LOGGER.error("Error while opening the valve: %s", err) return diff --git a/requirements_all.txt b/requirements_all.txt index beeb837a4e9..60a90d1e72c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aiofreepybox==0.0.8 aioftp==0.12.0 # homeassistant.components.guardian -aioguardian==0.2.3 +aioguardian==1.0.0 # homeassistant.components.harmony aioharmony==0.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5223644ba3..ad91ada704a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -85,7 +85,7 @@ aioesphomeapi==2.6.1 aiofreepybox==0.0.8 # homeassistant.components.guardian -aioguardian==0.2.3 +aioguardian==1.0.0 # homeassistant.components.harmony aioharmony==0.2.5 diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index 40df9c3cdb1..f54b285b960 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -9,7 +9,7 @@ def ping_client(): with patch( "homeassistant.components.guardian.async_setup_entry", return_value=True ), patch("aioguardian.client.Client.connect"), patch( - "aioguardian.commands.device.Device.ping", + "aioguardian.commands.system.SystemCommands.ping", return_value={"command": 0, "status": "ok", "data": {"uid": "ABCDEF123456"}}, ), patch( "aioguardian.client.Client.disconnect" From 584ce043e5c7ac520a7f3f280550d9798fc73774 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sun, 28 Jun 2020 00:19:54 -0500 Subject: [PATCH 047/428] Add debug output for invalid service call data (#37171) --- homeassistant/core.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index da4c5b56146..9e0b1514ab5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1219,7 +1219,16 @@ class ServiceRegistry: raise ServiceNotFound(domain, service) from None if handler.schema: - processed_data = handler.schema(service_data) + try: + processed_data = handler.schema(service_data) + except vol.Invalid: + _LOGGER.debug( + "Invalid data for service call %s.%s: %s", + domain, + service, + service_data, + ) + raise else: processed_data = service_data From 15165a3c938250ac325e2a9ca7c84dd6b02cb7b0 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sun, 28 Jun 2020 05:46:44 -0400 Subject: [PATCH 048/428] Fix issue with Insteon devices not responding to device changes (#37160) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 70a7375d51f..d1a31117fb9 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,6 +2,6 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["pyinsteon==1.0.4"], + "requirements": ["pyinsteon==1.0.5"], "codeowners": ["@teharris1"] } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 60a90d1e72c..5959cab25b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1388,7 +1388,7 @@ pyialarm==0.3 pyicloud==0.9.7 # homeassistant.components.insteon -pyinsteon==1.0.4 +pyinsteon==1.0.5 # homeassistant.components.intesishome pyintesishome==1.7.5 From 4a374f037864aad9330caaf39c48656058acb654 Mon Sep 17 00:00:00 2001 From: Gleb Sinyavskiy Date: Sun, 28 Jun 2020 13:56:54 +0200 Subject: [PATCH 049/428] Limit and sort transmission torrents_info attribute (#35411) --- .../components/transmission/__init__.py | 18 ++++-- .../components/transmission/config_flow.py | 22 ++++++- .../components/transmission/const.py | 22 ++++++- .../components/transmission/sensor.py | 57 +++++++++++++------ .../components/transmission/strings.json | 4 +- .../transmission/translations/en.json | 6 +- .../transmission/test_config_flow.py | 8 +++ tests/components/transmission/test_init.py | 4 +- 8 files changed, 114 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index a09fb6a7456..2fd7e60cf31 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -24,9 +24,13 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ATTR_DELETE_DATA, ATTR_TORRENT, + CONF_LIMIT, + CONF_ORDER, DATA_UPDATED, DEFAULT_DELETE_DATA, + DEFAULT_LIMIT, DEFAULT_NAME, + DEFAULT_ORDER, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, @@ -238,7 +242,13 @@ class TransmissionClient: scan_interval = self.config_entry.data.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ) - options = {CONF_SCAN_INTERVAL: scan_interval} + limit = self.config_entry.data.get(CONF_LIMIT, DEFAULT_LIMIT) + order = self.config_entry.data.get(CONF_ORDER, DEFAULT_ORDER) + options = { + CONF_SCAN_INTERVAL: scan_interval, + CONF_LIMIT: limit, + CONF_ORDER: order, + } self.hass.config_entries.async_update_entry( self.config_entry, options=options @@ -260,9 +270,9 @@ class TransmissionClient: @staticmethod async def async_options_updated(hass, entry): """Triggered by config entry options updates.""" - hass.data[DOMAIN][entry.entry_id].set_scan_interval( - entry.options[CONF_SCAN_INTERVAL] - ) + tm_client = hass.data[DOMAIN][entry.entry_id] + tm_client.set_scan_interval(entry.options[CONF_SCAN_INTERVAL]) + await hass.async_add_executor_job(tm_client.api.update) class TransmissionData: diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index 193c152d7c1..c457306310d 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -13,7 +13,17 @@ from homeassistant.const import ( from homeassistant.core import callback from . import get_api -from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import ( + CONF_LIMIT, + CONF_ORDER, + DEFAULT_LIMIT, + DEFAULT_NAME, + DEFAULT_ORDER, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + SUPPORTED_ORDER_MODES, +) from .errors import AuthenticationError, CannotConnect, UnknownError DATA_SCHEMA = vol.Schema( @@ -94,7 +104,15 @@ class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): default=self.config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), - ): int + ): int, + vol.Optional( + CONF_LIMIT, + default=self.config_entry.options.get(CONF_LIMIT, DEFAULT_LIMIT), + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=500)), + vol.Optional( + CONF_ORDER, + default=self.config_entry.options.get(CONF_ORDER, DEFAULT_ORDER), + ): vol.All(vol.Coerce(str), vol.In(SUPPORTED_ORDER_MODES.keys())), } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 8edbf944890..960fd7a65b4 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -1,10 +1,30 @@ """Constants for the Transmission Bittorent Client component.""" - DOMAIN = "transmission" SWITCH_TYPES = {"on_off": "Switch", "turtle_mode": "Turtle Mode"} +ORDER_NEWEST_FIRST = "newest_first" +ORDER_OLDEST_FIRST = "oldest_first" +ORDER_BEST_RATIO_FIRST = "best_ratio_first" +ORDER_WORST_RATIO_FIRST = "worst_ratio_first" + +SUPPORTED_ORDER_MODES = { + ORDER_NEWEST_FIRST: lambda torrents: sorted( + torrents, key=lambda t: t.addedDate, reverse=True + ), + ORDER_OLDEST_FIRST: lambda torrents: sorted(torrents, key=lambda t: t.addedDate), + ORDER_WORST_RATIO_FIRST: lambda torrents: sorted(torrents, key=lambda t: t.ratio), + ORDER_BEST_RATIO_FIRST: lambda torrents: sorted( + torrents, key=lambda t: t.ratio, reverse=True + ), +} + +CONF_LIMIT = "limit" +CONF_ORDER = "order" + DEFAULT_DELETE_DATA = False +DEFAULT_LIMIT = 10 +DEFAULT_ORDER = ORDER_OLDEST_FIRST DEFAULT_NAME = "Transmission" DEFAULT_PORT = 9091 DEFAULT_SCAN_INTERVAL = 120 diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 812d63f24d8..baa70bf0b19 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -6,7 +6,13 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DOMAIN, STATE_ATTR_TORRENT_INFO +from .const import ( + CONF_LIMIT, + CONF_ORDER, + DOMAIN, + STATE_ATTR_TORRENT_INFO, + SUPPORTED_ORDER_MODES, +) _LOGGER = logging.getLogger(__name__) @@ -143,28 +149,45 @@ class TransmissionTorrentsSensor(TransmissionSensor): @property def device_state_attributes(self): """Return the state attributes, if any.""" + limit = self._tm_client.config_entry.options[CONF_LIMIT] + order = self._tm_client.config_entry.options[CONF_ORDER] + torrents = self._tm_client.api.torrents[0:limit] info = _torrents_info( - self._tm_client.api.torrents, self.SUBTYPE_MODES[self._sub_type] + torrents, order=order, statuses=self.SUBTYPE_MODES[self._sub_type], ) - return {STATE_ATTR_TORRENT_INFO: info} + return { + STATE_ATTR_TORRENT_INFO: info, + } def update(self): """Get the latest data from Transmission and updates the state.""" - self._state = len(self.device_state_attributes[STATE_ATTR_TORRENT_INFO]) + torrents = _filter_torrents( + self._tm_client.api.torrents, statuses=self.SUBTYPE_MODES[self._sub_type] + ) + self._state = len(torrents) -def _torrents_info(torrents, statuses=None): +def _filter_torrents(torrents, statuses=None): + return [ + torrent + for torrent in torrents + if statuses is None or torrent.status in statuses + ] + + +def _torrents_info(torrents, order, statuses=None): infos = {} - for torrent in torrents: - if statuses is None or torrent.status in statuses: - info = infos[torrent.name] = { - "added_date": torrent.addedDate, - "percent_done": f"{torrent.percentDone * 100:.2f}", - "status": torrent.status, - "id": torrent.id, - } - try: - info["eta"] = str(torrent.eta) - except ValueError: - pass + torrents = _filter_torrents(torrents, statuses) + torrents = SUPPORTED_ORDER_MODES[order](torrents) + for torrent in _filter_torrents(torrents, statuses): + info = infos[torrent.name] = { + "added_date": torrent.addedDate, + "percent_done": f"{torrent.percentDone * 100:.2f}", + "status": torrent.status, + "id": torrent.id, + } + try: + info["eta"] = str(torrent.eta) + except ValueError: + pass return infos diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index e16ef43a48e..6d12258aff4 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -8,7 +8,9 @@ "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "port": "[%key:common::config_flow::data::port%]" + "port": "[%key:common::config_flow::data::port%]", + "limit": "Limit", + "order": "Order" } } }, diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index 702fda2dcd5..11ebfd61e1c 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/translations/en.json @@ -12,6 +12,8 @@ "user": { "data": { "host": "Host", + "limit": "Limit", + "order": "Order", "name": "Name", "password": "Password", "port": "Port", @@ -25,7 +27,9 @@ "step": { "init": { "data": { - "scan_interval": "Update frequency" + "scan_interval": "Update frequency", + "limit": "Limit", + "order": "Order" }, "title": "Configure options for Transmission" } diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 4436a6adf21..6b9917d17f4 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -8,7 +8,11 @@ from homeassistant import data_entry_flow from homeassistant.components import transmission from homeassistant.components.transmission import config_flow from homeassistant.components.transmission.const import ( + CONF_LIMIT, + CONF_ORDER, + DEFAULT_LIMIT, DEFAULT_NAME, + DEFAULT_ORDER, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, ) @@ -158,6 +162,8 @@ async def test_import(hass, api): CONF_HOST: HOST, CONF_PORT: DEFAULT_PORT, CONF_SCAN_INTERVAL: timedelta(seconds=DEFAULT_SCAN_INTERVAL), + CONF_LIMIT: DEFAULT_LIMIT, + CONF_ORDER: DEFAULT_ORDER, } ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -176,6 +182,8 @@ async def test_import(hass, api): CONF_PASSWORD: PASSWORD, CONF_PORT: PORT, CONF_SCAN_INTERVAL: timedelta(seconds=SCAN_INTERVAL), + CONF_LIMIT: DEFAULT_LIMIT, + CONF_ORDER: DEFAULT_ORDER, } ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 4baa00de7a7..78fddc5be86 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -81,7 +81,9 @@ async def test_successful_config_entry(hass, api): assert await transmission.async_setup_entry(hass, entry) is True assert entry.options == { - transmission.CONF_SCAN_INTERVAL: transmission.DEFAULT_SCAN_INTERVAL + transmission.CONF_SCAN_INTERVAL: transmission.DEFAULT_SCAN_INTERVAL, + transmission.CONF_LIMIT: transmission.DEFAULT_LIMIT, + transmission.CONF_ORDER: transmission.DEFAULT_ORDER, } From 76fa581bb930e85a0b93e952e52d0601f8a5b9c6 Mon Sep 17 00:00:00 2001 From: Gleb Sinyavskiy Date: Sun, 28 Jun 2020 22:51:18 +0200 Subject: [PATCH 050/428] Move transmission limit and order config options to the options flow (#37198) --- homeassistant/components/transmission/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 6d12258aff4..66a5b757818 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -8,9 +8,7 @@ "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "port": "[%key:common::config_flow::data::port%]", - "limit": "Limit", - "order": "Order" + "port": "[%key:common::config_flow::data::port%]" } } }, @@ -28,7 +26,9 @@ "init": { "title": "Configure options for Transmission", "data": { - "scan_interval": "Update frequency" + "scan_interval": "Update frequency", + "limit": "Limit", + "order": "Order" } } } From af5374d38b3b3cf1950b0ac3959058db5a623ecd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 29 Jun 2020 00:03:32 +0000 Subject: [PATCH 051/428] [ci skip] Translation update --- homeassistant/components/transmission/translations/en.json | 6 +++--- homeassistant/components/transmission/translations/es.json | 2 ++ homeassistant/components/transmission/translations/ru.json | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index 11ebfd61e1c..42ff361555c 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/translations/en.json @@ -13,8 +13,8 @@ "data": { "host": "Host", "limit": "Limit", - "order": "Order", "name": "Name", + "order": "Order", "password": "Password", "port": "Port", "username": "Username" @@ -27,9 +27,9 @@ "step": { "init": { "data": { - "scan_interval": "Update frequency", "limit": "Limit", - "order": "Order" + "order": "Order", + "scan_interval": "Update frequency" }, "title": "Configure options for Transmission" } diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 9626b5bd863..886a5afc773 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -12,7 +12,9 @@ "user": { "data": { "host": "Host", + "limit": "L\u00edmite", "name": "Nombre", + "order": "Pedido", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Usuario" diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index 17868c1e39e..0a850c68f17 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -12,7 +12,9 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442", + "limit": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "order": "\u041f\u043e\u0440\u044f\u0434\u043e\u043a", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "username": "\u041b\u043e\u0433\u0438\u043d" From ca265966e79b1196dca38e628992e1c7b03d3783 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 29 Jun 2020 06:39:56 +0200 Subject: [PATCH 052/428] Sensors sometimes are created without event (#37205) --- homeassistant/components/rfxtrx/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 1b053156fd8..9dbd3353734 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -64,7 +64,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): break for _data_type in data_types: new_sensor = RfxtrxSensor( - None, entity_info[ATTR_NAME], _data_type, entity_info[ATTR_FIRE_EVENT] + None, + event.device, + entity_info[ATTR_NAME], + _data_type, + entity_info[ATTR_FIRE_EVENT], ) sensors.append(new_sensor) sub_sensors[_data_type] = new_sensor @@ -107,7 +111,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if _data_type in event.values: data_type = _data_type break - new_sensor = RfxtrxSensor(event, pkt_id, data_type) + new_sensor = RfxtrxSensor(event, event.device, pkt_id, data_type) sub_sensors = {} sub_sensors[new_sensor.data_type] = new_sensor RFX_DEVICES[device_id] = sub_sensors @@ -120,14 +124,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class RfxtrxSensor(Entity): """Representation of a RFXtrx sensor.""" - def __init__(self, event, name, data_type, should_fire_event=False): + def __init__(self, event, device, name, data_type, should_fire_event=False): """Initialize the sensor.""" self.event = event self._name = name self.should_fire_event = should_fire_event self.data_type = data_type self._unit_of_measurement = DATA_TYPES.get(data_type, "") - self._unique_id = f"{slugify(self.event.device.type_string.lower())}_{slugify(self.event.device.id_string.lower())}_{slugify(self.data_type)}" + self._unique_id = f"{slugify(device.type_string.lower())}_{slugify(device.id_string.lower())}_{slugify(data_type)}" def __str__(self): """Return the name of the sensor.""" From 5badbcb0122cb17f5d9ed18e72c2375b963397e8 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:57:23 +0100 Subject: [PATCH 053/428] Correct typo in input_number UI text (#37208) --- homeassistant/components/input_number/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 76526b73e92..4d69bf72eda 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -2,13 +2,13 @@ decrement: description: Decrement the value of an input number entity by its stepping. fields: entity_id: - description: Entity id of the input number the should be decremented. + description: Entity id of the input number that should be decremented. example: input_number.threshold increment: description: Increment the value of an input number entity by its stepping. fields: entity_id: - description: Entity id of the input number the should be incremented. + description: Entity id of the input number that should be incremented. example: input_number.threshold set_value: description: Set the value of an input number entity. From 7b4df98875755e8250bfbf522a29e0df0ac65329 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jun 2020 06:37:56 -0500 Subject: [PATCH 054/428] Silence spurious warning when HomeKit is already running (#37199) If homekit.start is called when homekit is already running we previous warned. Downgrade the warning to a debug message as nothing is actually wrong. --- homeassistant/components/homekit/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c5a921a9dd2..bdab5a8ad07 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -327,9 +327,12 @@ def _async_register_events_and_services(hass: HomeAssistant): if HOMEKIT not in hass.data[DOMAIN][entry_id]: continue homekit = hass.data[DOMAIN][entry_id][HOMEKIT] + if homekit.status == STATUS_RUNNING: + _LOGGER.debug("HomeKit is already running") + continue if homekit.status != STATUS_READY: _LOGGER.warning( - "HomeKit is not ready. Either it is already running or has " + "HomeKit is not ready. Either it is already starting up or has " "been stopped." ) continue From b96ce9c210bbc00fcce54e9ffcad1c914467efb3 Mon Sep 17 00:00:00 2001 From: mdegat01 Date: Mon, 29 Jun 2020 11:31:49 -0400 Subject: [PATCH 055/428] Additional testing for InfluxDB and some quality improvements (#37181) * refactoring and added tests to sensor * using caplog and not mocking main setup method in sensor tests --- .coveragerc | 1 - homeassistant/components/influxdb/__init__.py | 137 +++--- homeassistant/components/influxdb/const.py | 118 +++++ homeassistant/components/influxdb/sensor.py | 54 +-- tests/components/influxdb/test_init.py | 185 ++++++- tests/components/influxdb/test_sensor.py | 453 ++++++++++++++++++ 6 files changed, 827 insertions(+), 121 deletions(-) create mode 100644 homeassistant/components/influxdb/const.py create mode 100644 tests/components/influxdb/test_sensor.py diff --git a/.coveragerc b/.coveragerc index ef8ea722106..0da952504de 100644 --- a/.coveragerc +++ b/.coveragerc @@ -372,7 +372,6 @@ omit = homeassistant/components/ihc/* homeassistant/components/imap/sensor.py homeassistant/components/imap_email_content/sensor.py - homeassistant/components/influxdb/sensor.py homeassistant/components/insteon/* homeassistant/components/incomfort/* homeassistant/components/intesishome/* diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 94a68c25504..c6e7cd19da0 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -2,7 +2,6 @@ import logging import math import queue -import re import threading import time from typing import Dict @@ -16,16 +15,7 @@ import urllib3.exceptions import voluptuous as vol from homeassistant.const import ( - CONF_API_VERSION, - CONF_HOST, - CONF_PASSWORD, - CONF_PATH, - CONF_PORT, - CONF_SSL, - CONF_TOKEN, CONF_URL, - CONF_USERNAME, - CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, @@ -39,39 +29,49 @@ from homeassistant.helpers.entityfilter import ( convert_include_exclude_filter, ) +from .const import ( + API_VERSION_2, + BATCH_BUFFER_SIZE, + BATCH_TIMEOUT, + CLIENT_ERROR_V1_WITH_RETRY, + CLIENT_ERROR_V2_WITH_RETRY, + COMPONENT_CONFIG_SCHEMA_CONNECTION, + CONF_API_VERSION, + CONF_BUCKET, + CONF_COMPONENT_CONFIG, + CONF_COMPONENT_CONFIG_DOMAIN, + CONF_COMPONENT_CONFIG_GLOB, + CONF_DB_NAME, + CONF_DEFAULT_MEASUREMENT, + CONF_HOST, + CONF_ORG, + CONF_OVERRIDE_MEASUREMENT, + CONF_PASSWORD, + CONF_PATH, + CONF_PORT, + CONF_RETRY_COUNT, + CONF_SSL, + CONF_TAGS, + CONF_TAGS_ATTRIBUTES, + CONF_TOKEN, + CONF_USERNAME, + CONF_VERIFY_SSL, + CONNECTION_ERROR_WITH_RETRY, + DEFAULT_API_VERSION, + DEFAULT_HOST_V2, + DEFAULT_SSL_V2, + DOMAIN, + QUEUE_BACKLOG_SECONDS, + RE_DECIMAL, + RE_DIGIT_TAIL, + RETRY_DELAY, + RETRY_INTERVAL, + TIMEOUT, + WRITE_ERROR, +) + _LOGGER = logging.getLogger(__name__) -CONF_DB_NAME = "database" -CONF_BUCKET = "bucket" -CONF_ORG = "organization" -CONF_TAGS = "tags" -CONF_DEFAULT_MEASUREMENT = "default_measurement" -CONF_OVERRIDE_MEASUREMENT = "override_measurement" -CONF_TAGS_ATTRIBUTES = "tags_attributes" -CONF_COMPONENT_CONFIG = "component_config" -CONF_COMPONENT_CONFIG_GLOB = "component_config_glob" -CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain" -CONF_RETRY_COUNT = "max_retries" - -DEFAULT_DATABASE = "home_assistant" -DEFAULT_HOST_V2 = "us-west-2-1.aws.cloud2.influxdata.com" -DEFAULT_SSL_V2 = True -DEFAULT_BUCKET = "Home Assistant" -DEFAULT_VERIFY_SSL = True -DEFAULT_API_VERSION = "1" - -DOMAIN = "influxdb" -API_VERSION_2 = "2" -TIMEOUT = 5 -RETRY_DELAY = 20 -QUEUE_BACKLOG_SECONDS = 30 -RETRY_INTERVAL = 60 # seconds - -BATCH_TIMEOUT = 1 -BATCH_BUFFER_SIZE = 100 - -DB_CONNECTION_FAILURE_MSG = () - def create_influx_url(conf: Dict) -> Dict: """Build URL used from config inputs and default when necessary.""" @@ -120,26 +120,6 @@ def validate_version_specific_config(conf: Dict) -> Dict: return conf -COMPONENT_CONFIG_SCHEMA_CONNECTION = { - # Connection config for V1 and V2 APIs. - vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.All( - vol.Coerce(str), vol.In([DEFAULT_API_VERSION, API_VERSION_2]), - ), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PATH): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL): cv.boolean, - # Connection config for V1 API only. - vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, - vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, - vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - # Connection config for V2 API only. - vol.Inclusive(CONF_TOKEN, "v2_authentication"): cv.string, - vol.Inclusive(CONF_ORG, "v2_authentication"): cv.string, - vol.Optional(CONF_BUCKET, default=DEFAULT_BUCKET): cv.string, -} - _CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string}) _CONFIG_SCHEMA = INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( @@ -174,9 +154,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -RE_DIGIT_TAIL = re.compile(r"^[^\.]*\d+\.?\d+[^\.]*$") -RE_DECIMAL = re.compile(r"[^\d.]+") - def get_influx_connection(client_kwargs, bucket): """Create and check the correct influx connection for the API version.""" @@ -254,26 +231,20 @@ def setup(hass, config): influx = get_influx_connection(kwargs, bucket) if use_v2_api: write_api = influx.write_api(write_options=ASYNCHRONOUS) - except (exceptions.InfluxDBClientError, requests.exceptions.ConnectionError) as exc: - _LOGGER.error( - "Database host is not accessible due to '%s', please " - "check your entries in the configuration file (host, " - "port, etc.) and verify that the database exists and is " - "READ/WRITE. Retrying again in %s seconds.", - exc, - RETRY_INTERVAL, - ) + except ( + OSError, + requests.exceptions.ConnectionError, + urllib3.exceptions.HTTPError, + ) as exc: + _LOGGER.error(CONNECTION_ERROR_WITH_RETRY, exc) event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) return True - except (ApiException, urllib3.exceptions.HTTPError) as exc: - _LOGGER.error( - "Bucket is not accessible due to '%s', please " - "check your entries in the configuration file (url, org, " - "bucket, etc.) and verify that the org and bucket exist and the " - "provided token has WRITE access. Retrying again in %s seconds.", - exc, - RETRY_INTERVAL, - ) + except exceptions.InfluxDBClientError as exc: + _LOGGER.error(CLIENT_ERROR_V1_WITH_RETRY, exc) + event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) + return True + except ApiException as exc: + _LOGGER.error(CLIENT_ERROR_V2_WITH_RETRY, exc) event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) return True @@ -468,7 +439,7 @@ class InfluxThread(threading.Thread): time.sleep(RETRY_DELAY) else: if not self.write_errors: - _LOGGER.error("Write error: %s", err) + _LOGGER.error(WRITE_ERROR, json, err) self.write_errors += len(json) def run(self): diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py new file mode 100644 index 00000000000..b59ead3a849 --- /dev/null +++ b/homeassistant/components/influxdb/const.py @@ -0,0 +1,118 @@ +"""Constants for InfluxDB integration.""" +from datetime import timedelta +import re + +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_VERSION, + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_PORT, + CONF_SSL, + CONF_TOKEN, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +import homeassistant.helpers.config_validation as cv + +CONF_DB_NAME = "database" +CONF_BUCKET = "bucket" +CONF_ORG = "organization" +CONF_TAGS = "tags" +CONF_DEFAULT_MEASUREMENT = "default_measurement" +CONF_OVERRIDE_MEASUREMENT = "override_measurement" +CONF_TAGS_ATTRIBUTES = "tags_attributes" +CONF_COMPONENT_CONFIG = "component_config" +CONF_COMPONENT_CONFIG_GLOB = "component_config_glob" +CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain" +CONF_RETRY_COUNT = "max_retries" + +CONF_LANGUAGE = "language" +CONF_QUERIES = "queries" +CONF_QUERIES_FLUX = "queries_flux" +CONF_GROUP_FUNCTION = "group_function" +CONF_FIELD = "field" +CONF_MEASUREMENT_NAME = "measurement" +CONF_WHERE = "where" + +CONF_RANGE_START = "range_start" +CONF_RANGE_STOP = "range_stop" +CONF_FUNCTION = "function" +CONF_QUERY = "query" +CONF_IMPORTS = "imports" + +DEFAULT_DATABASE = "home_assistant" +DEFAULT_HOST_V2 = "us-west-2-1.aws.cloud2.influxdata.com" +DEFAULT_SSL_V2 = True +DEFAULT_BUCKET = "Home Assistant" +DEFAULT_VERIFY_SSL = True +DEFAULT_API_VERSION = "1" +DEFAULT_GROUP_FUNCTION = "mean" +DEFAULT_FIELD = "value" +DEFAULT_RANGE_START = "-15m" +DEFAULT_RANGE_STOP = "now()" + +DOMAIN = "influxdb" +API_VERSION_2 = "2" +TIMEOUT = 5 +RETRY_DELAY = 20 +QUEUE_BACKLOG_SECONDS = 30 +RETRY_INTERVAL = 60 # seconds +BATCH_TIMEOUT = 1 +BATCH_BUFFER_SIZE = 100 +LANGUAGE_INFLUXQL = "influxQL" +LANGUAGE_FLUX = "flux" +TEST_QUERY_V1 = "SHOW SERIES LIMIT 1;" +TEST_QUERY_V2 = "buckets() |> limit(n:1)" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +RE_DIGIT_TAIL = re.compile(r"^[^\.]*\d+\.?\d+[^\.]*$") +RE_DECIMAL = re.compile(r"[^\d.]+") + +CONNECTION_ERROR = ( + "Cannot connect to InfluxDB due to '%s'. " + "Please check that the provided connection details (host, port, etc.) are correct " + "and that your InfluxDB server is running and accessible." +) +CLIENT_ERROR_V2 = ( + "InfluxDB bucket is not accessible due to '%s'. " + "Please check that the bucket, org and token are correct and " + "that the token has the correct permissions set." +) +CLIENT_ERROR_V1 = ( + "InfluxDB database is not accessible due to '%s'. " + "Please check that the database, username and password are correct and " + "that the specified user has the correct permissions set." +) +WRITE_ERROR = "Could not write '%s' to influx due to '%s'." +QUERY_ERROR = ( + "Could not execute query '%s' due to '%s'. Check the syntax of your query." +) +RETRY_MESSAGE = f"Retrying again in {RETRY_INTERVAL} seconds." +CONNECTION_ERROR_WITH_RETRY = f"{CONNECTION_ERROR} {RETRY_MESSAGE}" +CLIENT_ERROR_V1_WITH_RETRY = f"{CLIENT_ERROR_V1} {RETRY_MESSAGE}" +CLIENT_ERROR_V2_WITH_RETRY = f"{CLIENT_ERROR_V2} {RETRY_MESSAGE}" + + +COMPONENT_CONFIG_SCHEMA_CONNECTION = { + # Connection config for V1 and V2 APIs. + vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.All( + vol.Coerce(str), vol.In([DEFAULT_API_VERSION, API_VERSION_2]), + ), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PATH): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_SSL): cv.boolean, + # Connection config for V1 API only. + vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, + vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, + vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + # Connection config for V2 API only. + vol.Inclusive(CONF_TOKEN, "v2_authentication"): cv.string, + vol.Inclusive(CONF_ORG, "v2_authentication"): cv.string, + vol.Optional(CONF_BUCKET, default=DEFAULT_BUCKET): cv.string, +} diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 0cf25c0b2f4..302bcde2373 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -1,5 +1,4 @@ """InfluxDB component which allows you to get data from an Influx database.""" -from datetime import timedelta import logging from typing import Dict @@ -30,40 +29,35 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from . import ( +from . import create_influx_url, validate_version_specific_config +from .const import ( API_VERSION_2, COMPONENT_CONFIG_SCHEMA_CONNECTION, CONF_BUCKET, CONF_DB_NAME, + CONF_FIELD, + CONF_GROUP_FUNCTION, + CONF_IMPORTS, + CONF_MEASUREMENT_NAME, CONF_ORG, + CONF_QUERIES, + CONF_QUERIES_FLUX, + CONF_QUERY, + CONF_RANGE_START, + CONF_RANGE_STOP, + CONF_WHERE, DEFAULT_API_VERSION, - create_influx_url, - validate_version_specific_config, + DEFAULT_FIELD, + DEFAULT_GROUP_FUNCTION, + DEFAULT_RANGE_START, + DEFAULT_RANGE_STOP, + MIN_TIME_BETWEEN_UPDATES, + TEST_QUERY_V1, + TEST_QUERY_V2, ) _LOGGER = logging.getLogger(__name__) -DEFAULT_GROUP_FUNCTION = "mean" -DEFAULT_FIELD = "value" - -CONF_QUERIES = "queries" -CONF_QUERIES_FLUX = "queries_flux" -CONF_GROUP_FUNCTION = "group_function" -CONF_FIELD = "field" -CONF_MEASUREMENT_NAME = "measurement" -CONF_WHERE = "where" - -CONF_RANGE_START = "range_start" -CONF_RANGE_STOP = "range_stop" -CONF_FUNCTION = "function" -CONF_QUERY = "query" -CONF_IMPORTS = "imports" - -DEFAULT_RANGE_START = "-15m" -DEFAULT_RANGE_STOP = "now()" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - _QUERY_SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, @@ -217,9 +211,7 @@ class InfluxSensor(Entity): try: if query_api is not None: - query_api.query( - f'from(bucket: "{bucket}") |> range(start: -1ms) |> keep(columns: ["_time"]) |> limit(n: 1)' - ) + query_api.query(TEST_QUERY_V2) self.connected = True self.data = InfluxSensorDataV2( query_api, @@ -232,7 +224,7 @@ class InfluxSensor(Entity): ) else: - influx.query("SHOW SERIES LIMIT 1;") + influx.query(TEST_QUERY_V1) self.connected = True self.data = InfluxSensorDataV1( influx, @@ -336,7 +328,7 @@ class InfluxSensorDataV2: try: tables = self.query_api.query(self.full_query) - except ApiException as exc: + except (OSError, ApiException) as exc: _LOGGER.error( "Could not execute query '%s' due to '%s', " "Check the syntax of your query", @@ -390,7 +382,7 @@ class InfluxSensorDataV1: try: points = list(self.influx.query(self.query).get_points()) - except exceptions.InfluxDBClientError as exc: + except (OSError, exceptions.InfluxDBClientError) as exc: _LOGGER.error( "Could not execute query '%s' due to '%s', " "Check the syntax of your query", diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 29247bec9c8..04486f8f9b3 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -5,6 +5,7 @@ import datetime import pytest import homeassistant.components.influxdb as influxdb +from homeassistant.components.influxdb.const import DEFAULT_BUCKET from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_OFF, @@ -17,6 +18,8 @@ from homeassistant.setup import async_setup_component from tests.async_mock import MagicMock, Mock, call, patch +INFLUX_PATH = "homeassistant.components.influxdb" +INFLUX_CLIENT_PATH = f"{INFLUX_PATH}.InfluxDBClient" BASE_V1_CONFIG = {} BASE_V2_CONFIG = { "api_version": influxdb.API_VERSION_2, @@ -38,8 +41,7 @@ def mock_batch_timeout(hass, monkeypatch): """Mock the event bus listener and the batch timeout for tests.""" hass.bus.listen = MagicMock() monkeypatch.setattr( - "homeassistant.components.influxdb.InfluxThread.batch_timeout", - Mock(return_value=0), + f"{INFLUX_PATH}.InfluxThread.batch_timeout", Mock(return_value=0), ) @@ -47,9 +49,9 @@ def mock_batch_timeout(hass, monkeypatch): def mock_client_fixture(request): """Patch the InfluxDBClient object with mock for version under test.""" if request.param == influxdb.API_VERSION_2: - client_target = "homeassistant.components.influxdb.InfluxDBClientV2" + client_target = f"{INFLUX_CLIENT_PATH}V2" else: - client_target = "homeassistant.components.influxdb.InfluxDBClient" + client_target = INFLUX_CLIENT_PATH with patch(client_target) as client: yield client @@ -59,7 +61,7 @@ def mock_client_fixture(request): def get_mock_call_fixture(request): """Get version specific lambda to make write API call mock.""" if request.param == influxdb.API_VERSION_2: - return lambda body: call(bucket=influxdb.DEFAULT_BUCKET, record=body) + return lambda body: call(bucket=DEFAULT_BUCKET, record=body) # pylint: disable=unnecessary-lambda return lambda body: call(body) @@ -144,7 +146,16 @@ async def test_setup_minimal_config(hass, mock_client, config_ext, get_write_api "mock_client, config_ext, get_write_api", [ (influxdb.DEFAULT_API_VERSION, {"username": "user"}, _get_write_api_mock_v1), - (influxdb.DEFAULT_API_VERSION, {"token": "token"}, _get_write_api_mock_v1), + ( + influxdb.DEFAULT_API_VERSION, + {"token": "token", "organization": "organization"}, + _get_write_api_mock_v1, + ), + ( + influxdb.API_VERSION_2, + {"api_version": influxdb.API_VERSION_2}, + _get_write_api_mock_v2, + ), ( influxdb.API_VERSION_2, {"api_version": influxdb.API_VERSION_2, "organization": "organization"}, @@ -1147,3 +1158,165 @@ async def test_event_listener_backlog_full( hass.data[influxdb.DOMAIN].block_till_done() assert get_write_api(mock_client).call_count == 0 + + +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_event_listener_attribute_name_conflict( + hass, mock_client, config_ext, get_write_api, get_mock_call +): + """Test the event listener when an attribute conflicts with another field.""" + handler_method = await _setup(hass, mock_client, config_ext, get_write_api) + + attrs = {"value": "value_str"} + state = MagicMock( + state=1, + domain="fake", + entity_id="fake.something", + object_id="something", + attributes=attrs, + ) + event = MagicMock(data={"new_state": state}, time_fired=12345) + body = [ + { + "measurement": "fake.something", + "tags": {"domain": "fake", "entity_id": "something"}, + "time": 12345, + "fields": {"value": 1, "value__str": "value_str"}, + } + ] + handler_method(event) + hass.data[influxdb.DOMAIN].block_till_done() + + write_api = get_write_api(mock_client) + assert write_api.call_count == 1 + assert write_api.call_args == get_mock_call(body) + + +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call, test_exception", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + ConnectionError("fail"), + ), + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + influxdb.exceptions.InfluxDBClientError("fail"), + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + ConnectionError("fail"), + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + influxdb.ApiException(), + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_connection_failure_on_startup( + hass, caplog, mock_client, config_ext, get_write_api, get_mock_call, test_exception +): + """Test the event listener when it fails to connect to Influx on startup.""" + write_api = get_write_api(mock_client) + write_api.side_effect = test_exception + config = {"influxdb": config_ext} + + with patch(f"{INFLUX_PATH}.event_helper") as event_helper: + assert await async_setup_component(hass, influxdb.DOMAIN, config) + await hass.async_block_till_done() + + assert ( + len([record for record in caplog.records if record.levelname == "ERROR"]) + == 1 + ) + event_helper.call_later.assert_called_once() + hass.bus.listen.assert_not_called() + + +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call, test_exception", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + influxdb.exceptions.InfluxDBClientError("fail", code=400), + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + influxdb.ApiException(status=400), + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_invalid_inputs_error( + hass, caplog, mock_client, config_ext, get_write_api, get_mock_call, test_exception +): + """ + Test the event listener when influx returns invalid inputs on write. + + The difference in error handling in this case is that we do not sleep + and try again, if an input is invalid it is logged and dropped. + + Note that this shouldn't actually occur, if its possible for the current + code to send an invalid input then it should be adjusted to stop that. + But Influx is an external service so there may be edge cases that + haven't been encountered yet. + """ + handler_method = await _setup(hass, mock_client, config_ext, get_write_api) + + write_api = get_write_api(mock_client) + write_api.side_effect = test_exception + state = MagicMock( + state=1, + domain="fake", + entity_id="fake.something", + object_id="something", + attributes={}, + ) + event = MagicMock(data={"new_state": state}, time_fired=12345) + + with patch(f"{INFLUX_PATH}.time.sleep") as sleep: + handler_method(event) + hass.data[influxdb.DOMAIN].block_till_done() + + write_api.assert_called_once() + assert ( + len([record for record in caplog.records if record.levelname == "ERROR"]) + == 1 + ) + sleep.assert_not_called() diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py new file mode 100644 index 00000000000..4ef42d0fa3a --- /dev/null +++ b/tests/components/influxdb/test_sensor.py @@ -0,0 +1,453 @@ +"""The tests for the InfluxDB sensor.""" +from dataclasses import dataclass +from typing import Dict, List, Type + +from influxdb.exceptions import InfluxDBClientError +from influxdb_client.rest import ApiException +import pytest +from voluptuous import Invalid + +from homeassistant.components.influxdb.const import ( + API_VERSION_2, + DEFAULT_API_VERSION, + DOMAIN, + TEST_QUERY_V1, + TEST_QUERY_V2, +) +from homeassistant.components.influxdb.sensor import PLATFORM_SCHEMA +import homeassistant.components.sensor as sensor +from homeassistant.const import STATE_UNKNOWN +from homeassistant.setup import async_setup_component + +from tests.async_mock import MagicMock, patch + +INFLUXDB_PATH = "homeassistant.components.influxdb" +INFLUXDB_CLIENT_PATH = f"{INFLUXDB_PATH}.sensor.InfluxDBClient" +INFLUXDB_SENSOR_PATH = f"{INFLUXDB_PATH}.sensor" + +BASE_V1_CONFIG = {} +BASE_V2_CONFIG = { + "api_version": API_VERSION_2, + "organization": "org", + "token": "token", +} + +BASE_V1_QUERY = { + "queries": [ + { + "name": "test", + "measurement": "measurement", + "where": "where", + "field": "field", + } + ], +} +BASE_V2_QUERY = {"queries_flux": [{"name": "test", "query": "query"}]} + + +@dataclass +class Record: + """Record in a Table.""" + + values: Dict + + +@dataclass +class Table: + """Table in an Influx 2 resultset.""" + + records: List[Type[Record]] + + +@pytest.fixture(name="mock_client") +def mock_client_fixture(request): + """Patch the InfluxDBClient object with mock for version under test.""" + if request.param == API_VERSION_2: + client_target = f"{INFLUXDB_CLIENT_PATH}V2" + else: + client_target = INFLUXDB_CLIENT_PATH + + with patch(client_target) as client: + yield client + + +@pytest.fixture(autouse=True) +def mock_influx_platform(): + """ + Mock the influx client and queue in the main platform. + + Successful sensor setup is really independent of the main platform. + But since its one integration there is an internal dependency. + Mocking the client library there prevents failures and mocking the queue + to return `None` on get makes the listener shutdown immediately after initialization. + """ + with patch(f"{INFLUXDB_PATH}.InfluxDBClient") as mock_v1_client, patch( + f"{INFLUXDB_PATH}.InfluxDBClientV2" + ) as mock_v2_client, patch( + f"{INFLUXDB_PATH}.queue.Queue.get", return_value=None + ) as queue_get: + yield (mock_v1_client, mock_v2_client, queue_get) + + +@pytest.fixture(autouse=True, scope="module") +def mock_client_close(): + """Mock close method of clients at module scope.""" + with patch(f"{INFLUXDB_CLIENT_PATH}.close") as close_v1, patch( + f"{INFLUXDB_CLIENT_PATH}V2.close" + ) as close_v2: + yield (close_v1, close_v2) + + +def _make_v1_resultset(*args): + """Create a mock V1 resultset.""" + for arg in args: + yield {"value": arg} + + +def _make_v2_resultset(*args): + """Create a mock V2 resultset.""" + tables = [] + + for arg in args: + values = {"_value": arg} + record = Record(values) + tables.append(Table([record])) + + return tables + + +def _set_query_mock_v1(mock_influx_client, return_value=None, side_effect=None): + """Set return value or side effect for the V1 client.""" + query_api = mock_influx_client.return_value.query + if side_effect: + query_api.side_effect = side_effect + + else: + if return_value is None: + return_value = [] + + def get_return_value(query, **kwargs): + """Return mock for test query, return value otherwise.""" + if query == TEST_QUERY_V1: + return MagicMock() + + query_output = MagicMock() + query_output.get_points.return_value = return_value + return query_output + + query_api.side_effect = get_return_value + + +def _set_query_mock_v2(mock_influx_client, return_value=None, side_effect=None): + """Set return value or side effect for the V2 client.""" + query_api = mock_influx_client.return_value.query_api.return_value.query + if side_effect: + query_api.side_effect = side_effect + else: + if return_value is None: + return_value = [] + + query_api.return_value = return_value + + +async def _setup(hass, config_ext, queries, expected_sensors): + """Create client and test expected sensors.""" + config = { + DOMAIN: {}, + sensor.DOMAIN: {"platform": DOMAIN}, + } + influx_config = config[sensor.DOMAIN] + influx_config.update(config_ext) + influx_config.update(queries) + + assert await async_setup_component(hass, sensor.DOMAIN, config) + await hass.async_block_till_done() + + sensors = [] + for expected_sensor in expected_sensors: + state = hass.states.get(expected_sensor) + assert state is not None + sensors.append(state) + + return sensors + + +@pytest.mark.parametrize( + "mock_client, config_ext, queries", + [ + (DEFAULT_API_VERSION, BASE_V1_CONFIG, BASE_V1_QUERY), + (API_VERSION_2, BASE_V2_CONFIG, BASE_V2_QUERY), + ], + indirect=["mock_client"], +) +async def test_minimal_config(hass, mock_client, config_ext, queries): + """Test the minimal config and defaults.""" + await _setup(hass, config_ext, queries, ["sensor.test"]) + + +@pytest.mark.parametrize( + "mock_client, config_ext, queries", + [ + ( + DEFAULT_API_VERSION, + { + "ssl": "true", + "host": "host", + "port": "9000", + "path": "path", + "username": "user", + "password": "pass", + "database": "db", + "verify_ssl": "true", + "queries": [ + { + "name": "test", + "unit_of_measurement": "unit", + "measurement": "measurement", + "where": "where", + "value_template": "value", + "database": "db2", + "group_function": "fn", + "field": "field", + } + ], + }, + {}, + ), + ( + API_VERSION_2, + { + "api_version": "2", + "ssl": "true", + "host": "host", + "port": "9000", + "path": "path", + "token": "token", + "organization": "org", + "bucket": "bucket", + "queries_flux": [ + { + "name": "test", + "unit_of_measurement": "unit", + "range_start": "start", + "range_stop": "end", + "group_function": "fn", + "bucket": "bucket2", + "imports": "import", + "query": "query", + } + ], + }, + {}, + ), + ], + indirect=["mock_client"], +) +async def test_full_config(hass, mock_client, config_ext, queries): + """Test the full config.""" + await _setup(hass, config_ext, queries, ["sensor.test"]) + + +@pytest.mark.parametrize("config_ext", [(BASE_V1_CONFIG), (BASE_V2_CONFIG)]) +async def test_config_failure(hass, config_ext): + """Test an invalid config.""" + config = {"platform": DOMAIN} + config.update(config_ext) + + with pytest.raises(Invalid): + PLATFORM_SCHEMA(config) + + +@pytest.mark.parametrize( + "mock_client, config_ext, queries, set_query_mock, make_resultset", + [ + ( + DEFAULT_API_VERSION, + BASE_V1_CONFIG, + BASE_V1_QUERY, + _set_query_mock_v1, + _make_v1_resultset, + ), + ( + API_VERSION_2, + BASE_V2_CONFIG, + BASE_V2_QUERY, + _set_query_mock_v2, + _make_v2_resultset, + ), + ], + indirect=["mock_client"], +) +async def test_state_matches_query_result( + hass, mock_client, config_ext, queries, set_query_mock, make_resultset +): + """Test state of sensor matches respone from query api.""" + set_query_mock(mock_client, return_value=make_resultset(42)) + + sensors = await _setup(hass, config_ext, queries, ["sensor.test"]) + + assert sensors[0].state == "42" + + +@pytest.mark.parametrize( + "mock_client, config_ext, queries, set_query_mock, make_resultset", + [ + ( + DEFAULT_API_VERSION, + BASE_V1_CONFIG, + BASE_V1_QUERY, + _set_query_mock_v1, + _make_v1_resultset, + ), + ( + API_VERSION_2, + BASE_V2_CONFIG, + BASE_V2_QUERY, + _set_query_mock_v2, + _make_v2_resultset, + ), + ], + indirect=["mock_client"], +) +async def test_state_matches_first_query_result_for_multiple_return( + hass, caplog, mock_client, config_ext, queries, set_query_mock, make_resultset +): + """Test state of sensor matches respone from query api.""" + set_query_mock(mock_client, return_value=make_resultset(42, "not used")) + + sensors = await _setup(hass, config_ext, queries, ["sensor.test"]) + assert sensors[0].state == "42" + assert ( + len([record for record in caplog.records if record.levelname == "WARNING"]) == 1 + ) + + +@pytest.mark.parametrize( + "mock_client, config_ext, queries, set_query_mock", + [ + (DEFAULT_API_VERSION, BASE_V1_CONFIG, BASE_V1_QUERY, _set_query_mock_v1,), + (API_VERSION_2, BASE_V2_CONFIG, BASE_V2_QUERY, _set_query_mock_v2), + ], + indirect=["mock_client"], +) +async def test_state_for_no_results( + hass, caplog, mock_client, config_ext, queries, set_query_mock +): + """Test state of sensor matches respone from query api.""" + set_query_mock(mock_client) + + sensors = await _setup(hass, config_ext, queries, ["sensor.test"]) + assert sensors[0].state == STATE_UNKNOWN + assert ( + len([record for record in caplog.records if record.levelname == "WARNING"]) == 1 + ) + + +@pytest.mark.parametrize( + "mock_client, config_ext, queries, set_query_mock, query_exception", + [ + ( + DEFAULT_API_VERSION, + BASE_V1_CONFIG, + BASE_V1_QUERY, + _set_query_mock_v1, + OSError("fail"), + ), + ( + DEFAULT_API_VERSION, + BASE_V1_CONFIG, + BASE_V1_QUERY, + _set_query_mock_v1, + InfluxDBClientError("fail"), + ), + ( + DEFAULT_API_VERSION, + BASE_V1_CONFIG, + BASE_V1_QUERY, + _set_query_mock_v1, + InfluxDBClientError("fail", code=400), + ), + ( + API_VERSION_2, + BASE_V2_CONFIG, + BASE_V2_QUERY, + _set_query_mock_v2, + OSError("fail"), + ), + ( + API_VERSION_2, + BASE_V2_CONFIG, + BASE_V2_QUERY, + _set_query_mock_v2, + ApiException(), + ), + ( + API_VERSION_2, + BASE_V2_CONFIG, + BASE_V2_QUERY, + _set_query_mock_v2, + ApiException(status=400), + ), + ], + indirect=["mock_client"], +) +async def test_error_querying_influx( + hass, caplog, mock_client, config_ext, queries, set_query_mock, query_exception +): + """Test behavior of sensor when influx returns error.""" + + def mock_query_error(query, **kwargs): + """Throw error for any query besides test query.""" + if query in [TEST_QUERY_V1, TEST_QUERY_V2]: + return MagicMock() + raise query_exception + + set_query_mock(mock_client, side_effect=mock_query_error) + + sensors = await _setup(hass, config_ext, queries, ["sensor.test"]) + assert sensors[0].state == STATE_UNKNOWN + assert ( + len([record for record in caplog.records if record.levelname == "ERROR"]) == 1 + ) + + +@pytest.mark.parametrize( + "mock_client, config_ext, queries, set_query_mock, make_resultset", + [ + ( + DEFAULT_API_VERSION, + BASE_V1_CONFIG, + { + "queries": [ + { + "name": "test", + "measurement": "measurement", + "where": "{{ illegal.template }}", + "field": "field", + } + ] + }, + _set_query_mock_v1, + _make_v1_resultset, + ), + ( + API_VERSION_2, + BASE_V2_CONFIG, + {"queries_flux": [{"name": "test", "query": "{{ illegal.template }}"}]}, + _set_query_mock_v2, + _make_v2_resultset, + ), + ], + indirect=["mock_client"], +) +async def test_error_rendering_template( + hass, caplog, mock_client, config_ext, queries, set_query_mock, make_resultset +): + """Test behavior of sensor with error rendering template.""" + set_query_mock(mock_client, return_value=make_resultset(42)) + + sensors = await _setup(hass, config_ext, queries, ["sensor.test"]) + assert sensors[0].state == STATE_UNKNOWN + assert ( + len([record for record in caplog.records if record.levelname == "ERROR"]) == 1 + ) From 7ef33a72198ecc83d2dd3faa32980baf8fb6ed51 Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Mon, 29 Jun 2020 12:07:43 -0400 Subject: [PATCH 056/428] Add first unit test to config flow for Plum Lightpad (#37183) * add first unit test to config flow for Plum Lightpad * add first unit test to config flow for Plum Lightpad (add changed requirements_test_all.txt) * add first unit test to config flow for Plum Lightpad * add first unit test to config flow for Plum Lightpad (bring coverage to 100%) * add first unit test to config flow for Plum Lightpad * add first unit test to config flow for Plum Lightpad (updated patch path as suggested) * add first unit test to config flow for Plum Lightpad (add unit test for abort) --- .coveragerc | 3 +- requirements_test_all.txt | 3 + tests/components/plum_lightpad/__init__.py | 1 + .../plum_lightpad/test_config_flow.py | 117 ++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tests/components/plum_lightpad/__init__.py create mode 100644 tests/components/plum_lightpad/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0da952504de..150867054b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -620,7 +620,8 @@ omit = homeassistant/components/plugwise/climate.py homeassistant/components/plugwise/sensor.py homeassistant/components/plugwise/switch.py - homeassistant/components/plum_lightpad/* + homeassistant/components/plum_lightpad/__init__.py + homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* homeassistant/components/prezzibenzina/sensor.py diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad91ada704a..55b76e1d26b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -489,6 +489,9 @@ plexauth==0.0.5 # homeassistant.components.plex plexwebsocket==0.0.11 +# homeassistant.components.plum_lightpad +plumlightpad==0.0.11 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/tests/components/plum_lightpad/__init__.py b/tests/components/plum_lightpad/__init__.py new file mode 100644 index 00000000000..d86bcb13eb1 --- /dev/null +++ b/tests/components/plum_lightpad/__init__.py @@ -0,0 +1 @@ +"""Tests for the Plum Lightpad integration.""" diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py new file mode 100644 index 00000000000..9fc32f9872c --- /dev/null +++ b/tests/components/plum_lightpad/test_config_flow.py @@ -0,0 +1,117 @@ +"""Test the Plum Lightpad config flow.""" +from requests.exceptions import ConnectTimeout + +from homeassistant import config_entries, setup +from homeassistant.components.plum_lightpad.const import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" + ), patch( + "homeassistant.components.plum_lightpad.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.plum_lightpad.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-plum-username", "password": "test-plum-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-plum-username" + assert result2["data"] == { + "username": "test-plum-username", + "password": "test-plum-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData", + side_effect=ConnectTimeout, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-plum-username", "password": "test-plum-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_one_entry_per_email_allowed(hass): + """Test that only one entry allowed per Plum cloud email address.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="test-plum-username", + data={"username": "test-plum-username", "password": "test-plum-password"}, + ).add_to_hass(hass) + + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" + ), patch("homeassistant.components.plum_lightpad.async_setup") as mock_setup, patch( + "homeassistant.components.plum_lightpad.async_setup_entry" + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-plum-username", "password": "test-plum-password"}, + ) + + assert result2["type"] == "abort" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_import(hass): + """Test configuring the flow using configuration.yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" + ), patch( + "homeassistant.components.plum_lightpad.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.plum_lightpad.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"username": "test-plum-username", "password": "test-plum-password"}, + ) + assert result["type"] == "create_entry" + assert result["title"] == "test-plum-username" + assert result["data"] == { + "username": "test-plum-username", + "password": "test-plum-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 0f7200809039a2a5b73ad384085da9712139f85a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jun 2020 11:25:26 -0500 Subject: [PATCH 057/428] Ensure homekit state changed listeners are unsubscribed on reload (#37200) * Ensure homekit state changed listeners are unsubscribed on reload * fix mocking --- homeassistant/components/homekit/__init__.py | 2 ++ .../components/homekit/accessories.py | 33 +++++++++++++------ tests/components/homekit/test_accessories.py | 17 ++++++++++ tests/components/homekit/test_homekit.py | 2 ++ 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index bdab5a8ad07..de774fbf500 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -576,6 +576,8 @@ class HomeKit: self.status = STATUS_STOPPED _LOGGER.debug("Driver stop for %s", self._name) self.hass.add_job(self.driver.stop) + for acc in self.bridge.accessories.values(): + acc.async_stop() @callback def _async_configure_linked_sensors(self, ent_reg_ent, device_lookup, state): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index cb1c76656bb..0077f0bb018 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -270,6 +270,7 @@ class HomeAccessory(Accessory): self.entity_id = entity_id self.hass = hass self.debounce = {} + self._subscriptions = [] self._char_battery = None self._char_charging = None self._char_low_battery = None @@ -343,8 +344,10 @@ class HomeAccessory(Accessory): """ state = self.hass.states.get(self.entity_id) self.async_update_state_callback(None, None, state) - async_track_state_change( - self.hass, self.entity_id, self.async_update_state_callback + self._subscriptions.append( + async_track_state_change( + self.hass, self.entity_id, self.async_update_state_callback + ) ) battery_charging_state = None @@ -357,10 +360,12 @@ class HomeAccessory(Accessory): battery_charging_state = linked_battery_sensor_state.attributes.get( ATTR_BATTERY_CHARGING ) - async_track_state_change( - self.hass, - self.linked_battery_sensor, - self.async_update_linked_battery_callback, + self._subscriptions.append( + async_track_state_change( + self.hass, + self.linked_battery_sensor, + self.async_update_linked_battery_callback, + ) ) else: battery_state = state.attributes.get(ATTR_BATTERY_LEVEL) @@ -369,10 +374,12 @@ class HomeAccessory(Accessory): self.hass.states.get(self.linked_battery_charging_sensor).state == STATE_ON ) - async_track_state_change( - self.hass, - self.linked_battery_charging_sensor, - self.async_update_linked_battery_charging_callback, + self._subscriptions.append( + async_track_state_change( + self.hass, + self.linked_battery_charging_sensor, + self.async_update_linked_battery_charging_callback, + ) ) elif battery_charging_state is None: battery_charging_state = state.attributes.get(ATTR_BATTERY_CHARGING) @@ -481,6 +488,12 @@ class HomeAccessory(Accessory): self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data) await self.hass.services.async_call(domain, service, service_data) + @ha_callback + def async_stop(self): + """Cancel any subscriptions when the bridge is stopped.""" + while self._subscriptions: + self._subscriptions.pop(0)() + class HomeBridge(Bridge): """Adapter class for Bridge.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index abc6d9b5528..fd99230f206 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -45,6 +45,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, __version__, ) +from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS import homeassistant.util.dt as dt_util from tests.async_mock import Mock, patch @@ -83,6 +84,22 @@ async def test_debounce(hass): assert counter == 2 +async def test_accessory_cancels_track_state_change_on_stop(hass, hk_driver): + """Ensure homekit state changed listeners are unsubscribed on reload.""" + entity_id = "sensor.accessory" + hass.states.async_set(entity_id, None) + acc = HomeAccessory( + hass, hk_driver, "Home Accessory", entity_id, 2, {"platform": "isy994"} + ) + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + await acc.run_handler() + assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS][entity_id]) == 1 + acc.async_stop() + assert entity_id not in hass.data[TRACK_STATE_CHANGE_CALLBACKS] + + async def test_home_accessory(hass, hk_driver): """Test HomeAccessory class.""" entity_id = "sensor.accessory" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 1903e3eca8f..954758fe3f5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -605,6 +605,8 @@ async def test_homekit_stop(hass): entry_id=entry.entry_id, ) homekit.driver = Mock() + homekit.bridge = Mock() + homekit.bridge.accessories = {} await async_init_integration(hass) From 89a9634d35c1a2d3b563a54d45df8ef951ab2486 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jun 2020 11:39:24 -0500 Subject: [PATCH 058/428] Use eventloop for scheduling (#37184) Co-authored-by: Paulus Schoutsen --- homeassistant/components/flux/switch.py | 5 +- homeassistant/components/statistics/sensor.py | 12 +- homeassistant/components/sun/__init__.py | 14 ++- homeassistant/components/tod/binary_sensor.py | 9 +- homeassistant/const.py | 2 +- homeassistant/helpers/event.py | 28 ++--- homeassistant/helpers/update_coordinator.py | 4 +- tests/common.py | 18 ++- tests/components/asuswrt/test_sensor.py | 37 +++--- tests/components/automation/test_sun.py | 8 +- tests/components/cert_expiry/test_sensors.py | 29 ++--- tests/components/cloud/test_google_config.py | 4 +- tests/components/device_tracker/test_init.py | 12 +- tests/components/duckdns/test_init.py | 8 +- tests/components/flux/test_switch.py | 26 ++-- .../generic_thermostat/test_climate.py | 35 +++--- .../here_travel_time/test_sensor.py | 16 ++- tests/components/homekit/test_accessories.py | 12 +- .../components/homekit_controller/conftest.py | 5 +- .../islamic_prayer_times/test_init.py | 5 +- .../islamic_prayer_times/test_sensor.py | 3 +- .../jewish_calendar/test_binary_sensor.py | 11 +- .../components/jewish_calendar/test_sensor.py | 16 ++- .../manual/test_alarm_control_panel.py | 2 +- .../manual_mqtt/test_alarm_control_panel.py | 2 +- tests/components/metoffice/test_sensor.py | 5 +- tests/components/metoffice/test_weather.py | 8 +- .../mikrotik/test_device_tracker.py | 2 +- tests/components/moon/test_sensor.py | 65 ++++++---- tests/components/mqtt/test_binary_sensor.py | 10 +- tests/components/mqtt/test_sensor.py | 7 +- tests/components/pilight/test_init.py | 13 +- tests/components/push/test_camera.py | 5 +- .../pvpc_hourly_pricing/test_config_flow.py | 4 +- .../pvpc_hourly_pricing/test_sensor.py | 2 +- tests/components/rflink/test_binary_sensor.py | 2 +- tests/components/statistics/test_sensor.py | 13 +- tests/components/sun/test_init.py | 4 +- tests/components/tod/test_binary_sensor.py | 7 ++ tests/components/yr/test_sensor.py | 9 +- tests/conftest.py | 44 ++++++- tests/helpers/test_event.py | 115 +++++++++--------- 42 files changed, 366 insertions(+), 272 deletions(-) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 8a27c99c78d..4d45f217a59 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -31,8 +31,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers import config_validation as cv, event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify @@ -224,7 +223,7 @@ class FluxSwitch(SwitchEntity, RestoreEntity): if self.is_on: return - self.unsub_tracker = async_track_time_interval( + self.unsub_tracker = event.async_track_time_interval( self.hass, self.async_flux_update, datetime.timedelta(seconds=self._interval), diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 00e029e49c6..906b0a2c68b 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -17,12 +17,8 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, event from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import ( - async_track_point_in_utc_time, - async_track_state_change, -) from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -116,11 +112,11 @@ class StatisticsSensor(Entity): self.async_schedule_update_ha_state(True) @callback - def async_stats_sensor_startup(event): + def async_stats_sensor_startup(_): """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) - async_track_state_change( + event.async_track_state_change( self.hass, self._entity_id, async_stats_sensor_state_listener ) @@ -296,7 +292,7 @@ class StatisticsSensor(Entity): self.async_schedule_update_ha_state(True) self._update_listener = None - self._update_listener = async_track_point_in_utc_time( + self._update_listener = event.async_track_point_in_utc_time( self.hass, _scheduled_update, next_to_purge_timestamp ) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 9529a9c0cad..c4692598447 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -9,8 +9,8 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, ) from homeassistant.core import callback +from homeassistant.helpers import event from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.sun import ( get_astral_location, get_location_astral_event_next, @@ -99,7 +99,7 @@ class Sun(Entity): self.rising = self.phase = None self._next_change = None - def update_location(event): + def update_location(_event): self.location = get_astral_location(self.hass) self.update_events(dt_util.utcnow()) @@ -135,9 +135,9 @@ class Sun(Entity): STATE_ATTR_RISING: self.rising, } - def _check_event(self, utc_point_in_time, event, before): + def _check_event(self, utc_point_in_time, sun_event, before): next_utc = get_location_astral_event_next( - self.location, event, utc_point_in_time + self.location, sun_event, utc_point_in_time ) if next_utc < self._next_change: self._next_change = next_utc @@ -207,7 +207,9 @@ class Sun(Entity): self.update_sun_position(utc_point_in_time) # Set timer for the next solar event - async_track_point_in_utc_time(self.hass, self.update_events, self._next_change) + event.async_track_point_in_utc_time( + self.hass, self.update_events, self._next_change + ) _LOGGER.debug("next time: %s", self._next_change.isoformat()) @callback @@ -232,6 +234,6 @@ class Sun(Entity): # position update just drop it if utc_point_in_time + delta * 1.25 > self._next_change: return - async_track_point_in_utc_time( + event.async_track_point_in_utc_time( self.hass, self.update_sun_position, utc_point_in_time + delta ) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 8a5bbf16c6c..fde26acf604 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -14,8 +14,7 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, ) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers import config_validation as cv, event from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_next from homeassistant.util import dt as dt_util @@ -55,9 +54,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([sensor]) -def is_sun_event(event): +def is_sun_event(sun_event): """Return true if event is sun event not time.""" - return event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) + return sun_event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) class TodSensor(BinarySensorEntity): @@ -236,6 +235,6 @@ class TodSensor(BinarySensorEntity): self._calculate_next_update() self.async_write_ha_state() - async_track_point_in_utc_time( + event.async_track_point_in_utc_time( self.hass, self._point_in_time_listener, self.next_update ) diff --git a/homeassistant/const.py b/homeassistant/const.py index fe90ccf8504..3472e46dd69 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ MINOR_VERSION = 113 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 7, 0) +REQUIRED_PYTHON_VER = (3, 7, 1) # Truthy date string triggers showing related deprecation warning messages. REQUIRED_NEXT_PYTHON_VER = (3, 8, 0) REQUIRED_NEXT_PYTHON_DATE = "" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index a5f433b0e23..84e418e5eb0 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import functools as ft import logging +import time from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Union import attr @@ -316,26 +317,21 @@ def async_track_point_in_utc_time( point_in_time = dt_util.as_utc(point_in_time) @callback - def point_in_time_listener(event: Event) -> None: + def point_in_time_listener() -> None: """Listen for matching time_changed events.""" - now = event.data[ATTR_NOW] + hass.async_run_job(action, point_in_time) - if now < point_in_time or hasattr(point_in_time_listener, "run"): - return + cancel_callback = hass.loop.call_at( + hass.loop.time() + point_in_time.timestamp() - time.time(), + point_in_time_listener, + ) - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # 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. - setattr(point_in_time_listener, "run", True) - async_unsub() + @callback + def unsub_point_in_time_listener() -> None: + """Cancel the call_later.""" + cancel_callback.cancel() - hass.async_run_job(action, now) - - async_unsub = hass.bus.async_listen(EVENT_TIME_CHANGED, point_in_time_listener) - - return async_unsub + return unsub_point_in_time_listener track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_time) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 7fb96fdf7c8..3c8a5d2924e 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -8,7 +8,7 @@ from typing import Any, Awaitable, Callable, List, Optional import aiohttp from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers import event from homeassistant.util.dt import utcnow from .debounce import Debouncer @@ -99,7 +99,7 @@ class DataUpdateCoordinator: # minimizing the time between the point and the real activation. # That way we obtain a constant update frequency, # as long as the update process takes less than a second - self._unsub_refresh = async_track_point_in_utc_time( + self._unsub_refresh = event.async_track_point_in_utc_time( self.hass, self._handle_refresh_interval, utcnow().replace(microsecond=0) + self.update_interval, diff --git a/tests/common.py b/tests/common.py index dfe832cc4ce..03ab5b0e0a0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -11,6 +11,7 @@ import logging import os import sys import threading +import time import uuid from aiohttp.test_utils import unused_port as get_test_instance_port # noqa @@ -284,9 +285,22 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @ha.callback -def async_fire_time_changed(hass, time): +def async_fire_time_changed(hass, datetime_): """Fire a time changes event.""" - hass.bus.async_fire(EVENT_TIME_CHANGED, {"now": date_util.as_utc(time)}) + hass.bus.async_fire(EVENT_TIME_CHANGED, {"now": date_util.as_utc(datetime_)}) + + for task in list(hass.loop._scheduled): + if not isinstance(task, asyncio.TimerHandle): + continue + if task.cancelled(): + continue + + future_seconds = task.when() - hass.loop.time() + mock_seconds_into_future = datetime_.timestamp() - time.time() + + if mock_seconds_into_future >= future_seconds: + task._run() + task.cancel() fire_time_changed = threadsafe_callback_factory(async_fire_time_changed) diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 6d58b909280..6de3f1b2dcb 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -1,5 +1,5 @@ """The tests for the AsusWrt sensor platform.""" -from datetime import datetime, timedelta +from datetime import timedelta from aioasuswrt.asuswrt import Device @@ -16,7 +16,6 @@ from homeassistant.components.asuswrt import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util from homeassistant.util.dt import utcnow from tests.async_mock import AsyncMock, patch @@ -51,7 +50,7 @@ MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] -async def test_sensors(hass: HomeAssistant): +async def test_sensors(hass: HomeAssistant, mock_device_tracker_conf): """Test creating an AsusWRT sensor.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: AsusWrt().connection.async_connect = AsyncMock() @@ -61,23 +60,17 @@ async def test_sensors(hass: HomeAssistant): return_value=MOCK_CURRENT_TRANSFER_RATES ) - now = datetime(2020, 1, 1, 1, tzinfo=dt_util.UTC) - with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_ROUTER_SSH) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_ROUTER_SSH) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() - assert ( - hass.states.get(f"{sensor.DOMAIN}.asuswrt_devices_connected").state - == "3" - ) - assert ( - hass.states.get(f"{sensor.DOMAIN}.asuswrt_download_speed").state - == "160.0" - ) - assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_download").state == "60.0" - assert ( - hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload_speed").state == "80.0" - ) - assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload").state == "50.0" + assert ( + hass.states.get(f"{sensor.DOMAIN}.asuswrt_devices_connected").state == "3" + ) + assert ( + hass.states.get(f"{sensor.DOMAIN}.asuswrt_download_speed").state == "160.0" + ) + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_download").state == "60.0" + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload_speed").state == "80.0" + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload").state == "50.0" diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 4efb19ff201..f730dae3cf1 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -37,7 +37,7 @@ def teardown(): dt_util.set_default_time_zone(ORIG_TIME_ZONE) -async def test_sunset_trigger(hass, calls): +async def test_sunset_trigger(hass, calls, legacy_patchable_time): """Test the sunset trigger.""" now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, tzinfo=dt_util.UTC) @@ -70,7 +70,7 @@ async def test_sunset_trigger(hass, calls): assert len(calls) == 1 -async def test_sunrise_trigger(hass, calls): +async def test_sunrise_trigger(hass, calls, legacy_patchable_time): """Test the sunrise trigger.""" now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 14, tzinfo=dt_util.UTC) @@ -92,7 +92,7 @@ async def test_sunrise_trigger(hass, calls): assert len(calls) == 1 -async def test_sunset_trigger_with_offset(hass, calls): +async def test_sunset_trigger_with_offset(hass, calls, legacy_patchable_time): """Test the sunset trigger with offset.""" now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, 30, tzinfo=dt_util.UTC) @@ -125,7 +125,7 @@ async def test_sunset_trigger_with_offset(hass, calls): assert calls[0].data["some"] == "sun - sunset - 0:30:00" -async def test_sunrise_trigger_with_offset(hass, calls): +async def test_sunrise_trigger_with_offset(hass, calls, legacy_patchable_time): """Test the sunrise trigger with offset.""" now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC) diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 7896da9e74b..76c6716411b 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -6,7 +6,7 @@ import ssl from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE, STATE_UNKNOWN -import homeassistant.util.dt as dt_util +from homeassistant.util.dt import utcnow from .const import HOST, PORT from .helpers import future_timestamp, static_datetime @@ -91,7 +91,7 @@ async def test_async_setup_entry_host_unavailable(hass): assert entry.state == ENTRY_STATE_SETUP_RETRY - next_update = dt_util.utcnow() + timedelta(seconds=45) + next_update = utcnow() + timedelta(seconds=45) async_fire_time_changed(hass, next_update) with patch( "homeassistant.components.cert_expiry.helper.get_cert", @@ -115,8 +115,6 @@ async def test_update_sensor(hass): timestamp = future_timestamp(100) with patch("homeassistant.util.dt.utcnow", return_value=starting_time), patch( - "homeassistant.helpers.update_coordinator.utcnow", return_value=starting_time - ), patch( "homeassistant.components.cert_expiry.get_cert_expiry_timestamp", return_value=timestamp, ): @@ -139,14 +137,11 @@ async def test_update_sensor(hass): assert state.attributes.get("is_valid") next_update = starting_time + timedelta(hours=24) - with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch( - "homeassistant.helpers.update_coordinator.utcnow", return_value=next_update - ), patch( "homeassistant.components.cert_expiry.get_cert_expiry_timestamp", return_value=timestamp, ): - async_fire_time_changed(hass, next_update) + async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) await hass.async_block_till_done() state = hass.states.get("sensor.cert_expiry_example_com") @@ -176,8 +171,6 @@ async def test_update_sensor_network_errors(hass): timestamp = future_timestamp(100) with patch("homeassistant.util.dt.utcnow", return_value=starting_time), patch( - "homeassistant.helpers.update_coordinator.utcnow", return_value=starting_time - ), patch( "homeassistant.components.cert_expiry.get_cert_expiry_timestamp", return_value=timestamp, ): @@ -202,12 +195,10 @@ async def test_update_sensor_network_errors(hass): next_update = starting_time + timedelta(hours=24) with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch( - "homeassistant.helpers.update_coordinator.utcnow", return_value=next_update - ), patch( "homeassistant.components.cert_expiry.helper.get_cert", side_effect=socket.gaierror, ): - async_fire_time_changed(hass, next_update) + async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) await hass.async_block_till_done() next_update = starting_time + timedelta(hours=48) @@ -216,12 +207,10 @@ async def test_update_sensor_network_errors(hass): assert state.state == STATE_UNAVAILABLE with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch( - "homeassistant.helpers.update_coordinator.utcnow", return_value=next_update - ), patch( "homeassistant.components.cert_expiry.get_cert_expiry_timestamp", return_value=timestamp, ): - async_fire_time_changed(hass, next_update) + async_fire_time_changed(hass, utcnow() + timedelta(hours=48)) await hass.async_block_till_done() state = hass.states.get("sensor.cert_expiry_example_com") @@ -234,12 +223,10 @@ async def test_update_sensor_network_errors(hass): next_update = starting_time + timedelta(hours=72) with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch( - "homeassistant.helpers.update_coordinator.utcnow", return_value=next_update - ), patch( "homeassistant.components.cert_expiry.helper.get_cert", side_effect=ssl.SSLError("something bad"), ): - async_fire_time_changed(hass, next_update) + async_fire_time_changed(hass, utcnow() + timedelta(hours=72)) await hass.async_block_till_done() state = hass.states.get("sensor.cert_expiry_example_com") @@ -258,11 +245,9 @@ async def test_update_sensor_network_errors(hass): next_update = starting_time + timedelta(hours=96) with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch( - "homeassistant.helpers.update_coordinator.utcnow", return_value=next_update - ), patch( "homeassistant.components.cert_expiry.helper.get_cert", side_effect=Exception() ): - async_fire_time_changed(hass, next_update) + async_fire_time_changed(hass, utcnow() + timedelta(hours=96)) await hass.async_block_till_done() state = hass.states.get("sensor.cert_expiry_example_com") diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 3808f9b179c..5dd4afe883c 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -51,7 +51,9 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): assert len(mock_request_sync.mock_calls) == 1 -async def test_google_update_expose_trigger_sync(hass, cloud_prefs): +async def test_google_update_expose_trigger_sync( + hass, legacy_patchable_time, cloud_prefs +): """Test Google config responds to updating exposed entities.""" config = CloudGoogleConfig( hass, diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 1a366b0d2df..9424ed229b5 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -225,12 +225,14 @@ async def test_discover_platform(mock_demo_setup_scanner, mock_see, hass): async def test_update_stale(hass, mock_device_tracker_conf): """Test stalled update.""" + scanner = getattr(hass.components, "test.device_tracker").SCANNER scanner.reset() scanner.come_home("DEV1") - register_time = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) - scan_time = datetime(2015, 9, 15, 23, 1, tzinfo=dt_util.UTC) + now = dt_util.utcnow() + register_time = datetime(now.year + 1, 9, 15, 23, tzinfo=dt_util.UTC) + scan_time = datetime(now.year + 1, 9, 15, 23, 1, tzinfo=dt_util.UTC) with patch( "homeassistant.components.device_tracker.legacy.dt_util.utcnow", @@ -433,8 +435,10 @@ async def test_see_state(hass, yaml_devices): async def test_see_passive_zone_state(hass, mock_device_tracker_conf): """Test that the device tracker sets gps for passive trackers.""" - register_time = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) - scan_time = datetime(2015, 9, 15, 23, 1, tzinfo=dt_util.UTC) + now = dt_util.utcnow() + + register_time = datetime(now.year + 1, 9, 15, 23, tzinfo=dt_util.UTC) + scan_time = datetime(now.year + 1, 9, 15, 23, 1, tzinfo=dt_util.UTC) with assert_setup_component(1, zone.DOMAIN): zone_info = { diff --git a/tests/components/duckdns/test_init.py b/tests/components/duckdns/test_init.py index 9bc9b3504e7..03fce0df20e 100644 --- a/tests/components/duckdns/test_init.py +++ b/tests/components/duckdns/test_init.py @@ -159,7 +159,7 @@ async def test_async_track_time_interval_backoff(hass): _LOGGER.debug("Backoff...") for idx in range(1, len(intervals)): tme += intervals[idx] - async_fire_time_changed(hass, tme) + async_fire_time_changed(hass, tme + timedelta(seconds=0.1)) await hass.async_block_till_done() assert call_count == idx + 1 @@ -167,7 +167,7 @@ async def test_async_track_time_interval_backoff(hass): _LOGGER.debug("Max backoff reached - intervals[-1]") for _idx in range(1, 10): tme += intervals[-1] - async_fire_time_changed(hass, tme) + async_fire_time_changed(hass, tme + timedelta(seconds=0.1)) await hass.async_block_till_done() assert call_count == idx + 1 + _idx @@ -176,14 +176,14 @@ async def test_async_track_time_interval_backoff(hass): call_count = 0 ret_val = True tme += intervals[-1] - async_fire_time_changed(hass, tme) + async_fire_time_changed(hass, tme + timedelta(seconds=0.1)) await hass.async_block_till_done() assert call_count == 1 _LOGGER.debug("No backoff - intervals[0]") for _idx in range(2, 10): tme += intervals[0] - async_fire_time_changed(hass, tme) + async_fire_time_changed(hass, tme + timedelta(seconds=0.1)) await hass.async_block_till_done() assert call_count == _idx diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 0eada7da667..f10029a6469 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -282,7 +282,7 @@ async def test_flux_before_sunrise_known_location(hass): # pylint: disable=invalid-name -async def test_flux_after_sunrise_before_sunset(hass): +async def test_flux_after_sunrise_before_sunset(hass, legacy_patchable_time): """Test the flux switch after sunrise and before sunset.""" platform = getattr(hass.components, "test.light") platform.init() @@ -336,7 +336,7 @@ async def test_flux_after_sunrise_before_sunset(hass): # pylint: disable=invalid-name -async def test_flux_after_sunset_before_stop(hass): +async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time): """Test the flux switch after sunset and before stop.""" platform = getattr(hass.components, "test.light") platform.init() @@ -391,7 +391,7 @@ async def test_flux_after_sunset_before_stop(hass): # pylint: disable=invalid-name -async def test_flux_after_stop_before_sunrise(hass): +async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time): """Test the flux switch after stop and before sunrise.""" platform = getattr(hass.components, "test.light") platform.init() @@ -445,7 +445,7 @@ async def test_flux_after_stop_before_sunrise(hass): # pylint: disable=invalid-name -async def test_flux_with_custom_start_stop_times(hass): +async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time): """Test the flux with custom start and stop times.""" platform = getattr(hass.components, "test.light") platform.init() @@ -558,7 +558,9 @@ async def test_flux_before_sunrise_stop_next_day(hass): # pylint: disable=invalid-name -async def test_flux_after_sunrise_before_sunset_stop_next_day(hass): +async def test_flux_after_sunrise_before_sunset_stop_next_day( + hass, legacy_patchable_time +): """ Test the flux switch after sunrise and before sunset. @@ -618,7 +620,9 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(hass): # pylint: disable=invalid-name @pytest.mark.parametrize("x", [0, 1]) -async def test_flux_after_sunset_before_midnight_stop_next_day(hass, x): +async def test_flux_after_sunset_before_midnight_stop_next_day( + hass, legacy_patchable_time, x +): """Test the flux switch after sunset and before stop. This test has the stop_time on the next day (after midnight). @@ -792,7 +796,7 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(hass): # pylint: disable=invalid-name -async def test_flux_with_custom_colortemps(hass): +async def test_flux_with_custom_colortemps(hass, legacy_patchable_time): """Test the flux with custom start and stop colortemps.""" platform = getattr(hass.components, "test.light") platform.init() @@ -849,7 +853,7 @@ async def test_flux_with_custom_colortemps(hass): # pylint: disable=invalid-name -async def test_flux_with_custom_brightness(hass): +async def test_flux_with_custom_brightness(hass, legacy_patchable_time): """Test the flux with custom start and stop colortemps.""" platform = getattr(hass.components, "test.light") platform.init() @@ -904,7 +908,7 @@ async def test_flux_with_custom_brightness(hass): assert call.data[light.ATTR_XY_COLOR] == [0.506, 0.385] -async def test_flux_with_multiple_lights(hass): +async def test_flux_with_multiple_lights(hass, legacy_patchable_time): """Test the flux switch with multiple light entities.""" platform = getattr(hass.components, "test.light") platform.init() @@ -982,7 +986,7 @@ async def test_flux_with_multiple_lights(hass): assert call.data[light.ATTR_XY_COLOR] == [0.46, 0.376] -async def test_flux_with_mired(hass): +async def test_flux_with_mired(hass, legacy_patchable_time): """Test the flux switch´s mode mired.""" platform = getattr(hass.components, "test.light") platform.init() @@ -1034,7 +1038,7 @@ async def test_flux_with_mired(hass): assert call.data[light.ATTR_COLOR_TEMP] == 269 -async def test_flux_with_rgb(hass): +async def test_flux_with_rgb(hass, legacy_patchable_time): """Test the flux switch´s mode rgb.""" platform = getattr(hass.components, "test.light") platform.init() diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 5e144c3684a..313ff43ca6a 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -32,7 +32,11 @@ from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM from tests.async_mock import patch -from tests.common import assert_setup_component, mock_restore_cache +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + mock_restore_cache, +) from tests.components.climate import common ENTITY = "climate.test" @@ -949,13 +953,13 @@ async def test_temp_change_ac_trigger_on_long_enough_3(hass, setup_comp_7): await hass.async_block_till_done() await common.async_set_temperature(hass, 25) test_time = datetime.datetime.now(pytz.UTC) - _send_time_changed(hass, test_time) + async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert 0 == len(calls) - _send_time_changed(hass, test_time + datetime.timedelta(minutes=5)) + async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5)) await hass.async_block_till_done() assert 0 == len(calls) - _send_time_changed(hass, test_time + datetime.timedelta(minutes=10)) + async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10)) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -972,13 +976,13 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7): await hass.async_block_till_done() await common.async_set_temperature(hass, 25) test_time = datetime.datetime.now(pytz.UTC) - _send_time_changed(hass, test_time) + async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert 0 == len(calls) - _send_time_changed(hass, test_time + datetime.timedelta(minutes=5)) + async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5)) await hass.async_block_till_done() assert 0 == len(calls) - _send_time_changed(hass, test_time + datetime.timedelta(minutes=10)) + async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10)) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -987,11 +991,6 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7): assert ENT_SWITCH == call.data["entity_id"] -def _send_time_changed(hass, now): - """Send a time changed event.""" - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) - - @pytest.fixture async def setup_comp_8(hass): """Initialize components.""" @@ -1025,13 +1024,13 @@ async def test_temp_change_heater_trigger_on_long_enough_2(hass, setup_comp_8): await hass.async_block_till_done() await common.async_set_temperature(hass, 25) test_time = datetime.datetime.now(pytz.UTC) - _send_time_changed(hass, test_time) + async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert 0 == len(calls) - _send_time_changed(hass, test_time + datetime.timedelta(minutes=5)) + async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5)) await hass.async_block_till_done() assert 0 == len(calls) - _send_time_changed(hass, test_time + datetime.timedelta(minutes=10)) + async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10)) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -1048,13 +1047,13 @@ async def test_temp_change_heater_trigger_off_long_enough_2(hass, setup_comp_8): await hass.async_block_till_done() await common.async_set_temperature(hass, 25) test_time = datetime.datetime.now(pytz.UTC) - _send_time_changed(hass, test_time) + async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert 0 == len(calls) - _send_time_changed(hass, test_time + datetime.timedelta(minutes=5)) + async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5)) await hass.async_block_till_done() assert 0 == len(calls) - _send_time_changed(hass, test_time + datetime.timedelta(minutes=10)) + async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10)) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index c6240749aa3..386dbbdf0ee 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -567,7 +567,7 @@ async def test_bicycle(hass, requests_mock_credentials_check): assert sensor.attributes.get(ATTR_ICON) == ICON_BICYCLE -async def test_location_zone(hass, requests_mock_truck_response): +async def test_location_zone(hass, requests_mock_truck_response, legacy_patchable_time): """Test that origin/destination supplied by a zone works.""" utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. @@ -618,7 +618,9 @@ async def test_location_zone(hass, requests_mock_truck_response): _assert_truck_sensor(sensor) -async def test_location_sensor(hass, requests_mock_truck_response): +async def test_location_sensor( + hass, requests_mock_truck_response, legacy_patchable_time +): """Test that origin/destination supplied by a sensor works.""" utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. @@ -658,7 +660,9 @@ async def test_location_sensor(hass, requests_mock_truck_response): _assert_truck_sensor(sensor) -async def test_location_person(hass, requests_mock_truck_response): +async def test_location_person( + hass, requests_mock_truck_response, legacy_patchable_time +): """Test that origin/destination supplied by a person works.""" utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. @@ -707,7 +711,9 @@ async def test_location_person(hass, requests_mock_truck_response): _assert_truck_sensor(sensor) -async def test_location_device_tracker(hass, requests_mock_truck_response): +async def test_location_device_tracker( + hass, requests_mock_truck_response, legacy_patchable_time +): """Test that origin/destination supplied by a device_tracker works.""" utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. @@ -757,7 +763,7 @@ async def test_location_device_tracker(hass, requests_mock_truck_response): async def test_location_device_tracker_added_after_update( - hass, requests_mock_truck_response, caplog + hass, requests_mock_truck_response, legacy_patchable_time, caplog ): """Test that device_tracker added after first update works.""" caplog.set_level(logging.ERROR) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index fd99230f206..b19e63d3b4c 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -2,7 +2,7 @@ This includes tests for all mock object types. """ -from datetime import datetime, timedelta +from datetime import timedelta import pytest @@ -37,9 +37,7 @@ from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, - ATTR_NOW, ATTR_SERVICE, - EVENT_TIME_CHANGED, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -49,7 +47,7 @@ from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS import homeassistant.util.dt as dt_util from tests.async_mock import Mock, patch -from tests.common import async_mock_service +from tests.common import async_fire_time_changed, async_mock_service async def test_debounce(hass): @@ -66,11 +64,11 @@ async def test_debounce(hass): debounce_demo = debounce(demo_func) assert debounce_demo.__name__ == "demo_func" - now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) + now = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=now): await hass.async_add_executor_job(debounce_demo, mock, "value") - hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + async_fire_time_changed(hass, now + timedelta(seconds=3)) await hass.async_block_till_done() assert counter == 1 assert len(arguments) == 2 @@ -79,7 +77,7 @@ async def test_debounce(hass): await hass.async_add_executor_job(debounce_demo, mock, "value") await hass.async_add_executor_job(debounce_demo, mock, "value") - hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + async_fire_time_changed(hass, now + timedelta(seconds=3)) await hass.async_block_till_done() assert counter == 2 diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index ac4a0b4b5d6..3b023e0da51 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -5,13 +5,16 @@ from unittest import mock from aiohomekit.testing import FakeController import pytest +import homeassistant.util.dt as dt_util + import tests.async_mock @pytest.fixture def utcnow(request): """Freeze time at a known point.""" - start_dt = datetime.datetime(2019, 1, 1, 0, 0, 0) + now = dt_util.utcnow() + start_dt = datetime.datetime(now.year + 1, 1, 1, 0, 0, 0) with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = start_dt yield dt_utcnow diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 9fb9333e045..f3d4351ae29 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -85,7 +85,7 @@ async def test_unload_entry(hass): assert islamic_prayer_times.DOMAIN not in hass.data -async def test_islamic_prayer_times_timestamp_format(hass): +async def test_islamic_prayer_times_timestamp_format(hass, legacy_patchable_time): """Test Islamic prayer times timestamp format.""" entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) entry.add_to_hass(hass) @@ -94,7 +94,6 @@ async def test_islamic_prayer_times_timestamp_format(hass): "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ), patch("homeassistant.util.dt.now", return_value=NOW): - await hass.config_entries.async_setup(entry.entry_id) assert ( @@ -103,7 +102,7 @@ async def test_islamic_prayer_times_timestamp_format(hass): ) -async def test_update(hass): +async def test_update(hass, legacy_patchable_time): """Test sensors are updated with new prayer times.""" entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) entry.add_to_hass(hass) diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 3ee6a59136a..13b69207cde 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -8,7 +8,7 @@ from tests.async_mock import patch from tests.common import MockConfigEntry -async def test_islamic_prayer_times_sensors(hass): +async def test_islamic_prayer_times_sensors(hass, legacy_patchable_time): """Test minimum Islamic prayer times configuration.""" entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) entry.add_to_hass(hass) @@ -17,7 +17,6 @@ async def test_islamic_prayer_times_sensors(hass): "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ), patch("homeassistant.util.dt.now", return_value=NOW): - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index a9ea2449c6f..b9b980d29c2 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -60,7 +60,16 @@ MELACHA_TEST_IDS = [ ids=MELACHA_TEST_IDS, ) async def test_issur_melacha_sensor( - hass, now, candle_lighting, havdalah, diaspora, tzname, latitude, longitude, result + hass, + legacy_patchable_time, + now, + candle_lighting, + havdalah, + diaspora, + tzname, + latitude, + longitude, + result, ): """Test Issur Melacha sensor output.""" time_zone = dt_util.get_time_zone(tzname) diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 59b6dc01313..60def2e09d2 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -144,7 +144,16 @@ TEST_IDS = [ ids=TEST_IDS, ) async def test_jewish_calendar_sensor( - hass, now, tzname, latitude, longitude, language, sensor, diaspora, result + hass, + legacy_patchable_time, + now, + tzname, + latitude, + longitude, + language, + sensor, + diaspora, + result, ): """Test Jewish calendar sensor output.""" time_zone = dt_util.get_time_zone(tzname) @@ -478,6 +487,7 @@ SHABBAT_TEST_IDS = [ ) async def test_shabbat_times_sensor( hass, + legacy_patchable_time, language, now, candle_lighting, @@ -553,7 +563,7 @@ OMER_TEST_IDS = [ @pytest.mark.parametrize(["test_time", "result"], OMER_PARAMS, ids=OMER_TEST_IDS) -async def test_omer_sensor(hass, test_time, result): +async def test_omer_sensor(hass, legacy_patchable_time, test_time, result): """Test Omer Count sensor output.""" test_time = hass.config.time_zone.localize(test_time) @@ -587,7 +597,7 @@ DAFYOMI_TEST_IDS = [ @pytest.mark.parametrize(["test_time", "result"], DAFYOMI_PARAMS, ids=DAFYOMI_TEST_IDS) -async def test_dafyomi_sensor(hass, test_time, result): +async def test_dafyomi_sensor(hass, legacy_patchable_time, test_time, result): """Test Daf Yomi sensor output.""" test_time = hass.config.time_zone.localize(test_time) diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index c1f7fd5a7e0..9cf76ebbc8f 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -1450,7 +1450,7 @@ async def test_armed_custom_bypass_with_specific_pending(hass): assert STATE_ALARM_ARMED_CUSTOM_BYPASS == hass.states.get(entity_id).state -async def test_arm_away_after_disabled_disarmed(hass): +async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time): """Test pending state with and without zero trigger time.""" assert await async_setup_component( hass, diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index aa318d61b84..87a887b0751 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -1348,7 +1348,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock): assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state -async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock): +async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time, mqtt_mock): """Test pending state with and without zero trigger time.""" assert await async_setup_component( hass, diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 70a66a3093c..5d6f2787861 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -23,9 +23,8 @@ from tests.common import MockConfigEntry, load_fixture "datapoint.Forecast.datetime.datetime", Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), ) -async def test_one_sensor_site_running(hass, requests_mock): +async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_time): """Test the Met Office sensor platform.""" - # all metoffice test data encapsulated in here mock_json = json.loads(load_fixture("metoffice.json")) all_sites = json.dumps(mock_json["all_sites"]) @@ -62,7 +61,7 @@ async def test_one_sensor_site_running(hass, requests_mock): "datapoint.Forecast.datetime.datetime", Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), ) -async def test_two_sensor_sites_running(hass, requests_mock): +async def test_two_sensor_sites_running(hass, requests_mock, legacy_patchable_time): """Test we handle two sets of sensors running for two different sites.""" # all metoffice test data encapsulated in here diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 08440798f47..05cec7ef46e 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -20,7 +20,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture "datapoint.Forecast.datetime.datetime", Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), ) -async def test_site_cannot_connect(hass, requests_mock): +async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): """Test we handle cannot connect error.""" requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text="") @@ -42,7 +42,7 @@ async def test_site_cannot_connect(hass, requests_mock): "datapoint.Forecast.datetime.datetime", Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), ) -async def test_site_cannot_update(hass, requests_mock): +async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): """Test we handle cannot connect error.""" # all metoffice test data encapsulated in here @@ -77,7 +77,7 @@ async def test_site_cannot_update(hass, requests_mock): "datapoint.Forecast.datetime.datetime", Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), ) -async def test_one_weather_site_running(hass, requests_mock): +async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_time): """Test the Met Office weather platform.""" # all metoffice test data encapsulated in here @@ -111,7 +111,7 @@ async def test_one_weather_site_running(hass, requests_mock): "datapoint.Forecast.datetime.datetime", Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), ) -async def test_two_weather_sites_running(hass, requests_mock): +async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_time): """Test we handle two different weather sites both running.""" # all metoffice test data encapsulated in here diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index 643f94a5ad5..4e8ea93ab30 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -39,7 +39,7 @@ async def test_platform_manually_configured(hass): assert mikrotik.DOMAIN not in hass.data -async def test_device_trackers(hass): +async def test_device_trackers(hass, legacy_patchable_time): """Test device_trackers created by mikrotik.""" # test devices are added from wireless list only diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index fe6e57dd9b6..59210c63b90 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -1,46 +1,59 @@ """The test for the moon sensor platform.""" from datetime import datetime -import unittest -from homeassistant.setup import setup_component +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.async_mock import patch -from tests.common import get_test_home_assistant DAY1 = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) DAY2 = datetime(2017, 1, 18, 1, tzinfo=dt_util.UTC) -class TestMoonSensor(unittest.TestCase): +async def test_moon_day1(hass): """Test the Moon sensor.""" + config = {"sensor": {"platform": "moon", "name": "moon_day1"}} - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() + await async_setup_component(hass, HA_DOMAIN, {}) + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() + assert hass.states.get("sensor.moon_day1") - @patch("homeassistant.components.moon.sensor.dt_util.utcnow", return_value=DAY1) - def test_moon_day1(self, mock_request): - """Test the Moon sensor.""" - config = {"sensor": {"platform": "moon", "name": "moon_day1"}} + with patch( + "homeassistant.components.moon.sensor.dt_util.utcnow", return_value=DAY1 + ): + await async_update_entity(hass, "sensor.moon_day1") - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + assert hass.states.get("sensor.moon_day1").state == "waxing_crescent" - state = self.hass.states.get("sensor.moon_day1") - assert state.state == "waxing_crescent" - @patch("homeassistant.components.moon.sensor.dt_util.utcnow", return_value=DAY2) - def test_moon_day2(self, mock_request): - """Test the Moon sensor.""" - config = {"sensor": {"platform": "moon", "name": "moon_day2"}} +async def test_moon_day2(hass): + """Test the Moon sensor.""" + config = {"sensor": {"platform": "moon", "name": "moon_day2"}} - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + await async_setup_component(hass, HA_DOMAIN, {}) + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() - state = self.hass.states.get("sensor.moon_day2") - assert state.state == "waning_gibbous" + assert hass.states.get("sensor.moon_day2") + + with patch( + "homeassistant.components.moon.sensor.dt_util.utcnow", return_value=DAY2 + ): + await async_update_entity(hass, "sensor.moon_day2") + + assert hass.states.get("sensor.moon_day2").state == "waning_gibbous" + + +async def async_update_entity(hass, entity_id): + """Run an update action for an entity.""" + await hass.services.async_call( + HA_DOMAIN, SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: entity_id}, blocking=True, + ) + await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 67bb6704339..ff1c5a35a11 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -52,7 +52,9 @@ DEFAULT_CONFIG = { } -async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires_availability_topic( + hass, mqtt_mock, legacy_patchable_time, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -82,7 +84,9 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, await expires_helper(hass, mqtt_mock, caplog) -async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires( + hass, mqtt_mock, legacy_patchable_time, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -520,7 +524,7 @@ async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( - hass, mqtt_mock, caplog + hass, mqtt_mock, legacy_patchable_time, caplog ): """Test that binary_sensor with expire_after set behaves correctly on discovery and discovery update.""" entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index f54a27e8805..8e52e1e6f10 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -70,7 +70,9 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): assert state.attributes.get("unit_of_measurement") == "fav unit" -async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires( + hass, mqtt_mock, legacy_patchable_time, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -91,7 +93,8 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): state = hass.states.get("sensor.test") assert state.state == "unknown" - now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) + realnow = dt_util.utcnow() + now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC) with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "100") diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index 5c2acc0d8f2..1e7e71f8236 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -6,13 +6,16 @@ import unittest import pytest -from homeassistant import core as ha from homeassistant.components import pilight from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util from tests.async_mock import patch -from tests.common import assert_setup_component, get_test_home_assistant +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + get_test_home_assistant, +) _LOGGER = logging.getLogger(__name__) @@ -196,13 +199,13 @@ class TestPilight(unittest.TestCase): service_data1["protocol"] = [service_data1["protocol"]] service_data2["protocol"] = [service_data2["protocol"]] - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + async_fire_time_changed(self.hass, dt_util.utcnow()) self.hass.block_till_done() error_log_call = mock_pilight_error.call_args_list[-1] assert str(service_data1) in str(error_log_call) new_time = dt_util.utcnow() + timedelta(seconds=5) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: new_time}) + async_fire_time_changed(self.hass, new_time) self.hass.block_till_done() error_log_call = mock_pilight_error.call_args_list[-1] assert str(service_data2) in str(error_log_call) @@ -407,6 +410,6 @@ class TestPilightCallrateThrottler(unittest.TestCase): for i in range(3): exp.append(i) shifted_time = now + (timedelta(seconds=delay + 0.1) * i) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: shifted_time}) + async_fire_time_changed(self.hass, shifted_time) self.hass.block_till_done() assert runs == exp diff --git a/tests/components/push/test_camera.py b/tests/components/push/test_camera.py index 8f4bb43045e..74d975fc57c 100644 --- a/tests/components/push/test_camera.py +++ b/tests/components/push/test_camera.py @@ -2,11 +2,12 @@ from datetime import timedelta import io -from homeassistant import core as ha from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from tests.common import async_fire_time_changed + async def test_bad_posting(hass, aiohttp_client): """Test that posting to wrong api endpoint fails.""" @@ -74,7 +75,7 @@ async def test_posting_url(hass, aiohttp_client): # await timeout shifted_time = dt_util.utcnow() + timedelta(seconds=15) - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: shifted_time}) + async_fire_time_changed(hass, shifted_time) await hass.async_block_till_done() # back to initial state diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index d76f74f64a1..5f72875c26a 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -15,7 +15,9 @@ from tests.common import date_util from tests.test_util.aiohttp import AiohttpClientMocker -async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): +async def test_config_flow( + hass, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker +): """ Test config flow for pvpc_hourly_pricing. diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index 781ce35b99f..57861b8b72b 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -29,7 +29,7 @@ async def _process_time_step( async def test_sensor_availability( - hass, caplog, pvpc_aioclient_mock: AiohttpClientMocker + hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker ): """Test sensor availability and handling of cloud access.""" hass.config.time_zone = timezone("Europe/Madrid") diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 6a5a0b7f0e2..e20d2554f97 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -116,7 +116,7 @@ async def test_entity_availability(hass, monkeypatch): assert hass.states.get("binary_sensor.test").state == STATE_OFF -async def test_off_delay(hass, monkeypatch): +async def test_off_delay(hass, legacy_patchable_time, monkeypatch): """Test off_delay option.""" # setup mocking rflink module event_callback, create, _, _ = await mock_rflink(hass, CONFIG, DOMAIN, monkeypatch) diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 2234df8e5fe..f013d374106 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -3,6 +3,8 @@ from datetime import datetime, timedelta import statistics import unittest +import pytest + from homeassistant.components import recorder from homeassistant.components.statistics.sensor import StatisticsSensor from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS @@ -18,6 +20,12 @@ from tests.common import ( from tests.components.recorder.common import wait_recording_done +@pytest.fixture(autouse=True) +def mock_legacy_time(legacy_patchable_time): + """Make time patchable for all the tests.""" + yield + + class TestStatisticsSensor(unittest.TestCase): """Test the Statistics sensor.""" @@ -36,10 +44,7 @@ class TestStatisticsSensor(unittest.TestCase): self.change = round(self.values[-1] - self.values[0], 2) self.average_change = round(self.change / (len(self.values) - 1), 2) self.change_rate = round(self.change / (60 * (self.count - 1)), 2) - - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() + self.addCleanup(self.hass.stop) def test_binary_sensor_source(self): """Test if source is a sensor.""" diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index e023814725b..56ac683582a 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -12,7 +12,7 @@ import homeassistant.util.dt as dt_util from tests.async_mock import patch -async def test_setting_rising(hass): +async def test_setting_rising(hass, legacy_patchable_time): """Test retrieving sun setting and rising.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=utc_now): @@ -103,7 +103,7 @@ async def test_setting_rising(hass): ) -async def test_state_change(hass): +async def test_state_change(hass, legacy_patchable_time): """Test if the state changes at next setting/rising.""" now = datetime(2016, 6, 1, 8, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=now): diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index afa299ef063..b776477be5c 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import unittest +import pytest import pytz from homeassistant import setup @@ -15,6 +16,12 @@ from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant +@pytest.fixture(autouse=True) +def mock_legacy_time(legacy_patchable_time): + """Make time patchable for all the tests.""" + yield + + class TestBinarySensorTod(unittest.TestCase): """Test for Binary sensor tod platform.""" diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index cb0345641b7..b339dd9c132 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -11,7 +11,7 @@ from tests.common import assert_setup_component, load_fixture NOW = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) -async def test_default_setup(hass, aioclient_mock): +async def test_default_setup(hass, legacy_patchable_time, aioclient_mock): """Test the default setup.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", @@ -19,6 +19,7 @@ async def test_default_setup(hass, aioclient_mock): ) config = {"platform": "yr", "elevation": 0} hass.allow_pool = True + with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): @@ -31,7 +32,7 @@ async def test_default_setup(hass, aioclient_mock): assert state.attributes.get("unit_of_measurement") is None -async def test_custom_setup(hass, aioclient_mock): +async def test_custom_setup(hass, legacy_patchable_time, aioclient_mock): """Test a custom setup.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", @@ -50,6 +51,7 @@ async def test_custom_setup(hass, aioclient_mock): ], } hass.allow_pool = True + with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): @@ -77,7 +79,7 @@ async def test_custom_setup(hass, aioclient_mock): assert state.state == "3.5" -async def test_forecast_setup(hass, aioclient_mock): +async def test_forecast_setup(hass, legacy_patchable_time, aioclient_mock): """Test a custom setup with 24h forecast.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", @@ -97,6 +99,7 @@ async def test_forecast_setup(hass, aioclient_mock): ], } hass.allow_pool = True + with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): diff --git a/tests/conftest.py b/tests/conftest.py index a2fa8e8b2fd..0d100afc275 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import logging import pytest import requests_mock as _requests_mock -from homeassistant import core as ha, util +from homeassistant import core as ha, loader, util from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.components import mqtt @@ -15,7 +15,9 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.http import URL +from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED from homeassistant.exceptions import ServiceNotFound +from homeassistant.helpers import event from homeassistant.setup import async_setup_component from homeassistant.util import location @@ -315,3 +317,43 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): component = hass.data["mqtt"] component.reset_mock() return component + + +@pytest.fixture +def legacy_patchable_time(): + """Allow time to be patchable by using event listeners instead of asyncio loop.""" + + @ha.callback + @loader.bind_hass + def async_track_point_in_utc_time(hass, action, point_in_time): + """Add a listener that fires once after a specific point in UTC time.""" + # Ensure point_in_time is UTC + point_in_time = event.dt_util.as_utc(point_in_time) + + @ha.callback + def point_in_time_listener(event): + """Listen for matching time_changed events.""" + now = event.data[ATTR_NOW] + + if now < point_in_time or hasattr(point_in_time_listener, "run"): + return + + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # 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. + setattr(point_in_time_listener, "run", True) + async_unsub() + + hass.async_run_job(action, now) + + async_unsub = hass.bus.async_listen(EVENT_TIME_CHANGED, point_in_time_listener) + + return async_unsub + + with patch( + "homeassistant.helpers.event.async_track_point_in_utc_time", + async_track_point_in_utc_time, + ): + yield diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 2d5ee9a9a73..eff0e6e973b 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -38,11 +38,6 @@ def teardown(): dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) -def _send_time_changed(hass, now): - """Send a time changed event.""" - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) - - async def test_track_point_in_time(hass): """Test track point in time.""" before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) @@ -55,16 +50,16 @@ async def test_track_point_in_time(hass): hass, callback(lambda x: runs.append(1)), birthday_paulus ) - _send_time_changed(hass, before_birthday) + async_fire_time_changed(hass, before_birthday) await hass.async_block_till_done() assert len(runs) == 0 - _send_time_changed(hass, birthday_paulus) + async_fire_time_changed(hass, birthday_paulus) await hass.async_block_till_done() assert len(runs) == 1 # A point in time tracker will only fire once, this should do nothing - _send_time_changed(hass, birthday_paulus) + async_fire_time_changed(hass, birthday_paulus) await hass.async_block_till_done() assert len(runs) == 1 @@ -72,7 +67,7 @@ async def test_track_point_in_time(hass): hass, callback(lambda x: runs.append(1)), birthday_paulus ) - _send_time_changed(hass, after_birthday) + async_fire_time_changed(hass, after_birthday) await hass.async_block_till_done() assert len(runs) == 2 @@ -81,7 +76,7 @@ async def test_track_point_in_time(hass): ) unsub() - _send_time_changed(hass, after_birthday) + async_fire_time_changed(hass, after_birthday) await hass.async_block_till_done() assert len(runs) == 2 @@ -458,26 +453,26 @@ async def test_track_time_interval(hass): hass, lambda x: specific_runs.append(1), timedelta(seconds=10) ) - _send_time_changed(hass, utc_now + timedelta(seconds=5)) + async_fire_time_changed(hass, utc_now + timedelta(seconds=5)) await hass.async_block_till_done() assert len(specific_runs) == 0 - _send_time_changed(hass, utc_now + timedelta(seconds=13)) + async_fire_time_changed(hass, utc_now + timedelta(seconds=13)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, utc_now + timedelta(minutes=20)) + async_fire_time_changed(hass, utc_now + timedelta(minutes=20)) await hass.async_block_till_done() assert len(specific_runs) == 2 unsub() - _send_time_changed(hass, utc_now + timedelta(seconds=30)) + async_fire_time_changed(hass, utc_now + timedelta(seconds=30)) await hass.async_block_till_done() assert len(specific_runs) == 2 -async def test_track_sunrise(hass): +async def test_track_sunrise(hass, legacy_patchable_time): """Test track the sunrise.""" latitude = 32.87336 longitude = 117.22743 @@ -514,17 +509,17 @@ async def test_track_sunrise(hass): unsub2 = async_track_sunrise(hass, lambda: offset_runs.append(1), offset) # run tests - _send_time_changed(hass, next_rising - offset) + async_fire_time_changed(hass, next_rising - offset) await hass.async_block_till_done() assert len(runs) == 0 assert len(offset_runs) == 0 - _send_time_changed(hass, next_rising) + async_fire_time_changed(hass, next_rising) await hass.async_block_till_done() assert len(runs) == 1 assert len(offset_runs) == 0 - _send_time_changed(hass, next_rising + offset) + async_fire_time_changed(hass, next_rising + offset) await hass.async_block_till_done() assert len(runs) == 1 assert len(offset_runs) == 1 @@ -532,13 +527,13 @@ async def test_track_sunrise(hass): unsub() unsub2() - _send_time_changed(hass, next_rising + offset) + async_fire_time_changed(hass, next_rising + offset) await hass.async_block_till_done() assert len(runs) == 1 assert len(offset_runs) == 1 -async def test_track_sunrise_update_location(hass): +async def test_track_sunrise_update_location(hass, legacy_patchable_time): """Test track the sunrise.""" # Setup sun component hass.config.latitude = 32.87336 @@ -567,7 +562,7 @@ async def test_track_sunrise_update_location(hass): async_track_sunrise(hass, lambda: runs.append(1)) # Mimic sunrise - _send_time_changed(hass, next_rising) + async_fire_time_changed(hass, next_rising) await hass.async_block_till_done() assert len(runs) == 1 @@ -577,7 +572,7 @@ async def test_track_sunrise_update_location(hass): await hass.async_block_till_done() # Mimic sunrise - _send_time_changed(hass, next_rising) + async_fire_time_changed(hass, next_rising) await hass.async_block_till_done() # Did not increase assert len(runs) == 1 @@ -593,12 +588,12 @@ async def test_track_sunrise_update_location(hass): mod += 1 # Mimic sunrise at new location - _send_time_changed(hass, next_rising) + async_fire_time_changed(hass, next_rising) await hass.async_block_till_done() assert len(runs) == 2 -async def test_track_sunset(hass): +async def test_track_sunset(hass, legacy_patchable_time): """Test track the sunset.""" latitude = 32.87336 longitude = 117.22743 @@ -635,17 +630,17 @@ async def test_track_sunset(hass): unsub2 = async_track_sunset(hass, lambda: offset_runs.append(1), offset) # Run tests - _send_time_changed(hass, next_setting - offset) + async_fire_time_changed(hass, next_setting - offset) await hass.async_block_till_done() assert len(runs) == 0 assert len(offset_runs) == 0 - _send_time_changed(hass, next_setting) + async_fire_time_changed(hass, next_setting) await hass.async_block_till_done() assert len(runs) == 1 assert len(offset_runs) == 0 - _send_time_changed(hass, next_setting + offset) + async_fire_time_changed(hass, next_setting + offset) await hass.async_block_till_done() assert len(runs) == 1 assert len(offset_runs) == 1 @@ -653,7 +648,7 @@ async def test_track_sunset(hass): unsub() unsub2() - _send_time_changed(hass, next_setting + offset) + async_fire_time_changed(hass, next_setting + offset) await hass.async_block_till_done() assert len(runs) == 1 assert len(offset_runs) == 1 @@ -669,17 +664,17 @@ async def test_async_track_time_change(hass): hass, lambda x: specific_runs.append(1), second=[0, 30] ) - _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 assert len(wildcard_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 15)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 15)) await hass.async_block_till_done() assert len(specific_runs) == 1 assert len(wildcard_runs) == 2 - _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 30)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 30)) await hass.async_block_till_done() assert len(specific_runs) == 2 assert len(wildcard_runs) == 3 @@ -687,7 +682,7 @@ async def test_async_track_time_change(hass): unsub() unsub_utc() - _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 30)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 30)) await hass.async_block_till_done() assert len(specific_runs) == 2 assert len(wildcard_runs) == 3 @@ -701,21 +696,21 @@ async def test_periodic_task_minute(hass): hass, lambda x: specific_runs.append(1), minute="/5", second=0 ) - _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 24, 12, 3, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 3, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 24, 12, 5, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 5, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 unsub() - _send_time_changed(hass, datetime(2014, 5, 24, 12, 5, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 5, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 @@ -728,29 +723,29 @@ async def test_periodic_task_hour(hass): hass, lambda x: specific_runs.append(1), hour="/2", minute=0, second=0 ) - _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 24, 23, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 23, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 25, 0, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 25, 0, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 - _send_time_changed(hass, datetime(2014, 5, 25, 1, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 25, 1, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 - _send_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 3 unsub() - _send_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 3 @@ -764,7 +759,7 @@ async def test_periodic_task_wrong_input(hass): hass, lambda x: specific_runs.append(1), hour="/two" ) - _send_time_changed(hass, datetime(2014, 5, 2, 0, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 2, 0, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 0 @@ -777,29 +772,29 @@ async def test_periodic_task_clock_rollback(hass): hass, lambda x: specific_runs.append(1), hour="/2", minute=0, second=0 ) - _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 24, 23, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 23, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 - _send_time_changed(hass, datetime(2014, 5, 24, 0, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 0, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 3 - _send_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 4 unsub() - _send_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 4 @@ -812,15 +807,15 @@ async def test_periodic_task_duplicate_time(hass): hass, lambda x: specific_runs.append(1), hour="/2", minute=0, second=0 ) - _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed(hass, datetime(2014, 5, 25, 0, 0, 0)) + async_fire_time_changed(hass, datetime(2014, 5, 25, 0, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 @@ -837,19 +832,19 @@ async def test_periodic_task_entering_dst(hass): hass, lambda x: specific_runs.append(1), hour=2, minute=30, second=0 ) - _send_time_changed(hass, timezone.localize(datetime(2018, 3, 25, 1, 50, 0))) + async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 25, 1, 50, 0))) await hass.async_block_till_done() assert len(specific_runs) == 0 - _send_time_changed(hass, timezone.localize(datetime(2018, 3, 25, 3, 50, 0))) + async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 25, 3, 50, 0))) await hass.async_block_till_done() assert len(specific_runs) == 0 - _send_time_changed(hass, timezone.localize(datetime(2018, 3, 26, 1, 50, 0))) + async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 26, 1, 50, 0))) await hass.async_block_till_done() assert len(specific_runs) == 0 - _send_time_changed(hass, timezone.localize(datetime(2018, 3, 26, 2, 50, 0))) + async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 26, 2, 50, 0))) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -866,25 +861,25 @@ async def test_periodic_task_leaving_dst(hass): hass, lambda x: specific_runs.append(1), hour=2, minute=30, second=0 ) - _send_time_changed( + async_fire_time_changed( hass, timezone.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False) ) await hass.async_block_till_done() assert len(specific_runs) == 0 - _send_time_changed( + async_fire_time_changed( hass, timezone.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False) ) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed( + async_fire_time_changed( hass, timezone.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True) ) await hass.async_block_till_done() assert len(specific_runs) == 1 - _send_time_changed( + async_fire_time_changed( hass, timezone.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True) ) await hass.async_block_till_done() From b0e2f5f375bcedcf654d9e1f45da14f7c28b8c66 Mon Sep 17 00:00:00 2001 From: mdegat01 Date: Mon, 29 Jun 2020 14:21:21 -0400 Subject: [PATCH 059/428] Add mdegat01 as code owner for InfluxDB (#37227) --- CODEOWNERS | 2 +- homeassistant/components/influxdb/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d860a0f57ed..f91f857ac1a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -193,7 +193,7 @@ homeassistant/components/iaqualink/* @flz homeassistant/components/icloud/* @Quentame homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/incomfort/* @zxdavb -homeassistant/components/influxdb/* @fabaff +homeassistant/components/influxdb/* @fabaff @mdegat01 homeassistant/components/input_boolean/* @home-assistant/core homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index 596c0ecc6ce..46d234e370c 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -3,5 +3,5 @@ "name": "InfluxDB", "documentation": "https://www.home-assistant.io/integrations/influxdb", "requirements": ["influxdb==5.2.3", "influxdb-client==1.6.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff", "@mdegat01"] } From cc7bed5dabce0835a6cc2832c4a41e3f3da80439 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 29 Jun 2020 22:45:28 +0300 Subject: [PATCH 060/428] Fix updating ping sensor (#37220) --- homeassistant/components/speedtestdotnet/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 3ddd75bb715..3cad15a0967 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -129,11 +129,10 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): server_id = self.config_entry.options.get(CONF_SERVER_ID) self.api.closest.clear() self.api.get_servers(servers=[server_id]) - self.api.get_best_server() _LOGGER.debug( "Executing speedtest.net speed test with server_id: %s", self.api.best["id"] ) - + self.api.get_best_server() self.api.download() self.api.upload() return self.api.results.dict() From 12510b0c979b754432f050ba2dd6607960e7c2d3 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Tue, 30 Jun 2020 02:36:52 +0400 Subject: [PATCH 061/428] Fixes after PR #36479 (#37230) --- homeassistant/components/mqtt/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 69d84705088..bb2ec7a8bcb 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -994,6 +994,9 @@ class MqttAvailability(Entity): await self._availability_subscribe_topics() async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) async_dispatcher_connect(self.hass, MQTT_DISCONNECTED, self.async_mqtt_connect) + self.async_on_remove( + async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) + ) async def availability_discovery_update(self, config: dict): """Handle updated discovery message.""" @@ -1029,7 +1032,8 @@ class MqttAvailability(Entity): @callback def async_mqtt_connect(self): """Update state on connection/disconnection to MQTT broker.""" - self.async_write_ha_state() + if self.hass.is_running: + self.async_write_ha_state() async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" From b0942d86fe3d19441193a38d6d00625be27a5b56 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Jun 2020 18:37:42 -0400 Subject: [PATCH 062/428] Bump ZHA Quirks to 0.0.41 (#37235) --- 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 c6dbeecf68b..c9b25b58e25 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "bellows==0.17.0", "pyserial==3.4", - "zha-quirks==0.0.40", + "zha-quirks==0.0.41", "zigpy-cc==0.4.4", "zigpy-deconz==0.9.2", "zigpy==0.21.0", diff --git a/requirements_all.txt b/requirements_all.txt index 5959cab25b4..d790760faf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2255,7 +2255,7 @@ zengge==0.2 zeroconf==0.27.1 # homeassistant.components.zha -zha-quirks==0.0.40 +zha-quirks==0.0.41 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55b76e1d26b..fb41a8998b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -964,7 +964,7 @@ ya_ma==0.3.8 zeroconf==0.27.1 # homeassistant.components.zha -zha-quirks==0.0.40 +zha-quirks==0.0.41 # homeassistant.components.zha zigpy-cc==0.4.4 From 11debb15685ecca2ca37ed380a7c3d9046be1541 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Mon, 29 Jun 2020 18:41:52 -0400 Subject: [PATCH 063/428] Fix wind speed change in NWS (#37222) --- homeassistant/components/nws/weather.py | 11 +++++------ tests/components/nws/const.py | 6 ++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 7e1ca37ab6b..f7890190490 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -190,17 +190,16 @@ class NWSWeather(WeatherEntity): @property def wind_speed(self): """Return the current windspeed.""" - wind_m_s = None + wind_km_hr = None if self.observation: - wind_m_s = self.observation.get("windSpeed") - if wind_m_s is None: + wind_km_hr = self.observation.get("windSpeed") + if wind_km_hr is None: return None - wind_m_hr = wind_m_s * 3600 if self.is_metric: - wind = convert_distance(wind_m_hr, LENGTH_METERS, LENGTH_KILOMETERS) + wind = wind_km_hr else: - wind = convert_distance(wind_m_hr, LENGTH_METERS, LENGTH_MILES) + wind = convert_distance(wind_km_hr, LENGTH_KILOMETERS, LENGTH_MILES) return round(wind) @property diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 6dee20a0759..8b23f9cc850 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -60,7 +60,7 @@ EXPECTED_OBSERVATION_IMPERIAL = { ), ATTR_WEATHER_WIND_BEARING: 180, ATTR_WEATHER_WIND_SPEED: round( - convert_distance(10, LENGTH_METERS, LENGTH_MILES) * 3600 + convert_distance(10, LENGTH_KILOMETERS, LENGTH_MILES) ), ATTR_WEATHER_PRESSURE: round( convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2 @@ -74,9 +74,7 @@ EXPECTED_OBSERVATION_IMPERIAL = { EXPECTED_OBSERVATION_METRIC = { ATTR_WEATHER_TEMPERATURE: 10, ATTR_WEATHER_WIND_BEARING: 180, - ATTR_WEATHER_WIND_SPEED: round( - convert_distance(10, LENGTH_METERS, LENGTH_KILOMETERS) * 3600 - ), + ATTR_WEATHER_WIND_SPEED: 10, ATTR_WEATHER_PRESSURE: round(convert_pressure(100000, PRESSURE_PA, PRESSURE_HPA)), ATTR_WEATHER_VISIBILITY: round( convert_distance(10000, LENGTH_METERS, LENGTH_KILOMETERS) From ac237ee10f326dac91b2fc51891d2f62dd924d05 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Jun 2020 15:54:02 -0700 Subject: [PATCH 064/428] Updated frontend to 20200629.0 (#37240) --- 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 6fc6bba73ff..81370e96511 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==20200626.1" + "home-assistant-frontend==20200629.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9bce53ba733..70957391911 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.34.7 -home-assistant-frontend==20200626.1 +home-assistant-frontend==20200629.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 netdisco==2.7.1 diff --git a/requirements_all.txt b/requirements_all.txt index d790760faf2..6eab3d3658e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200626.1 +home-assistant-frontend==20200629.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb41a8998b1..a62951f422f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200626.1 +home-assistant-frontend==20200629.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 79f131066c1772a51b738df2280f8cc09c8c3b74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Jun 2020 01:23:11 +0200 Subject: [PATCH 065/428] Ensure recorder data integrity and MySQL lock error handling (#37228) --- homeassistant/components/recorder/purge.py | 64 +++++++++++++--------- tests/components/recorder/test_purge.py | 2 +- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 8b0b71e24ae..19c2db47768 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -1,8 +1,9 @@ """Purge old data helper.""" from datetime import timedelta import logging +import time -from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.exc import OperationalError, SQLAlchemyError import homeassistant.util.dt as dt_util @@ -18,47 +19,46 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool: Cleans up an timeframe of an hour, based on the oldest record. """ purge_before = dt_util.utcnow() - timedelta(days=purge_days) - _LOGGER.debug("Purging events before %s", purge_before) + _LOGGER.debug("Purging states and events before target %s", purge_before) try: with session_scope(session=instance.get_session()) as session: + # Purge a max of 1 hour, based on the oldest states or events record + batch_purge_before = purge_before + query = session.query(States).order_by(States.last_updated.asc()).limit(1) states = execute(query, to_native=True, validate_entity_ids=False) - - states_purge_before = purge_before if states: - states_purge_before = min( - purge_before, states[0].last_updated + timedelta(hours=1) + batch_purge_before = min( + batch_purge_before, states[0].last_updated + timedelta(hours=1), ) - deleted_rows_states = ( - session.query(States) - .filter(States.last_updated < states_purge_before) - .delete(synchronize_session=False) - ) - _LOGGER.debug("Deleted %s states", deleted_rows_states) - query = session.query(Events).order_by(Events.time_fired.asc()).limit(1) events = execute(query, to_native=True) - - events_purge_before = purge_before if events: - events_purge_before = min( - purge_before, events[0].time_fired + timedelta(hours=1) + batch_purge_before = min( + batch_purge_before, events[0].time_fired + timedelta(hours=1), ) - deleted_rows_events = ( - session.query(Events) - .filter(Events.time_fired < events_purge_before) + _LOGGER.debug("Purging states and events before %s", batch_purge_before) + + deleted_rows = ( + session.query(States) + .filter(States.last_updated < batch_purge_before) .delete(synchronize_session=False) ) - _LOGGER.debug("Deleted %s events", deleted_rows_events) + _LOGGER.debug("Deleted %s states", deleted_rows) + + deleted_rows = ( + session.query(Events) + .filter(Events.time_fired < batch_purge_before) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s events", deleted_rows) # If states or events purging isn't processing the purge_before yet, # return false, as we are not done yet. - if (states_purge_before and states_purge_before != purge_before) or ( - events_purge_before and events_purge_before != purge_before - ): + if batch_purge_before != purge_before: _LOGGER.debug("Purging hasn't fully completed yet.") return False @@ -80,7 +80,21 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool: _LOGGER.debug("Optimizing SQL DB to free space") instance.engine.execute("OPTIMIZE TABLE states, events, recorder_runs") + except OperationalError as err: + # Retry when one of the following MySQL errors occurred: + # 1205: Lock wait timeout exceeded; try restarting transaction + # 1206: The total number of locks exceeds the lock table size + # 1213: Deadlock found when trying to get lock; try restarting transaction + if instance.engine.driver in ("mysqldb", "pymysql") and err.orig.args[0] in ( + 1205, + 1206, + 1213, + ): + _LOGGER.info("%s; purge not completed, retrying", err.orig.args[1]) + time.sleep(instance.db_retry_wait) + return False + + _LOGGER.warning("Error purging history: %s.", err) except SQLAlchemyError as err: _LOGGER.warning("Error purging history: %s.", err) - return True diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index afcb1b2818f..93fb6e51621 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -224,6 +224,6 @@ class TestRecorderPurge(unittest.TestCase): self.hass.block_till_done() self.hass.data[DATA_INSTANCE].block_till_done() assert ( - mock_logger.debug.mock_calls[4][1][0] + mock_logger.debug.mock_calls[5][1][0] == "Vacuuming SQL DB to free space" ) From bba47ad9b18d30b87ee1e0ee381cbe3980802a98 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 30 Jun 2020 00:06:54 +0000 Subject: [PATCH 066/428] [ci skip] Translation update --- .../components/awair/translations/ca.json | 3 ++- .../components/awair/translations/lb.json | 9 +++++++- .../components/denonavr/translations/lb.json | 2 ++ .../hvv_departures/translations/lb.json | 7 ++++-- .../components/metoffice/translations/lb.json | 10 ++++++++- .../components/smappee/translations/lb.json | 3 ++- .../speedtestdotnet/translations/lb.json | 10 +++++++++ .../squeezebox/translations/ca.json | 10 ++++++--- .../components/toon/translations/ca.json | 10 +++++++++ .../transmission/translations/ca.json | 4 ++++ .../transmission/translations/es.json | 2 ++ .../transmission/translations/lb.json | 4 ++++ .../transmission/translations/ru.json | 2 ++ .../transmission/translations/zh-Hant.json | 4 ++++ .../xiaomi_aqara/translations/ca.json | 22 +++++++++++++++---- 15 files changed, 89 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index 682fe89aa3b..076c12f7995 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -21,7 +21,8 @@ "data": { "access_token": "Token d'acc\u00e9s", "email": "Correu electr\u00f2nic" - } + }, + "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/awair/translations/lb.json b/homeassistant/components/awair/translations/lb.json index d9ef0d9c1a7..176a8984e8c 100644 --- a/homeassistant/components/awair/translations/lb.json +++ b/homeassistant/components/awair/translations/lb.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "already_configured": "Kont ass", + "no_devices": "Keng Apparater am Netzwierk fonnt", + "reauth_successful": "Acc\u00e8s Jeton erfollegr\u00e4ich aktualis\u00e9iert" + }, "error": { + "auth": "Ong\u00ebltege Acc\u00e8s Jeton", "unknown": "Onbekannten Awair API Feeler" }, "step": { @@ -15,7 +21,8 @@ "data": { "access_token": "Acc\u00e8s Jeton", "email": "E-Mail" - } + }, + "description": "Du muss dech fir een Awair Developpeur Acc\u00e8s Jeton registr\u00e9ien op:\nhttps://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/denonavr/translations/lb.json b/homeassistant/components/denonavr/translations/lb.json index 89a7b09c06e..8b2580209a1 100644 --- a/homeassistant/components/denonavr/translations/lb.json +++ b/homeassistant/components/denonavr/translations/lb.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", "already_in_progress": "Konfiguratioun fir d\u00ebsen Denon AVR ass schonn am gaang.", "connection_error": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", "not_denonavr_manufacturer": "Kee Denon AVR Netzwierk Empf\u00e4nger, entdeckte Hiersteller passt net", @@ -12,6 +13,7 @@ "flow_title": "Denon AVR Netzwierk Empf\u00e4nger: {name}", "step": { "confirm": { + "description": "Best\u00e4teg dob\u00e4isetzen vum Receiver", "title": "Denon AVR Netzwierk Empf\u00e4nger" }, "select": { diff --git a/homeassistant/components/hvv_departures/translations/lb.json b/homeassistant/components/hvv_departures/translations/lb.json index 5adb1d22b0a..fd624db30ce 100644 --- a/homeassistant/components/hvv_departures/translations/lb.json +++ b/homeassistant/components/hvv_departures/translations/lb.json @@ -36,10 +36,13 @@ "init": { "data": { "filter": "Zeilen auswielen", - "offset": "Offset (Minutten)" + "offset": "Offset (Minutten)", + "real_time": "Benotz Echtz\u00e4it Donn\u00e9e\u00ebn" }, + "description": "Optiounen \u00e4nneren fir d\u00ebsen Departe Sensor", "title": "Optiounen" } } - } + }, + "title": "HVV Departe" } \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/lb.json b/homeassistant/components/metoffice/translations/lb.json index 26ee66d9786..da135fc6b78 100644 --- a/homeassistant/components/metoffice/translations/lb.json +++ b/homeassistant/components/metoffice/translations/lb.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "unknown": "Onerwaarte Feeler" + }, "step": { "user": { "data": { @@ -7,7 +14,8 @@ "latitude": "Breedegrad", "longitude": "L\u00e4ngegrad" }, - "description": "L\u00e4ngegrad a Breedegrad gi benotzt fir d\u00e9i nooste Statioun auszewielen." + "description": "L\u00e4ngegrad a Breedegrad gi benotzt fir d\u00e9i nooste Statioun auszewielen.", + "title": "Mam UK Met Office verbannen" } } } diff --git a/homeassistant/components/smappee/translations/lb.json b/homeassistant/components/smappee/translations/lb.json index 8169e17a6de..a95770a058f 100644 --- a/homeassistant/components/smappee/translations/lb.json +++ b/homeassistant/components/smappee/translations/lb.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng Konfiguratioun ass m\u00e9iglech." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/speedtestdotnet/translations/lb.json b/homeassistant/components/speedtestdotnet/translations/lb.json index 9098d499a14..b9587e71087 100644 --- a/homeassistant/components/speedtestdotnet/translations/lb.json +++ b/homeassistant/components/speedtestdotnet/translations/lb.json @@ -3,12 +3,22 @@ "abort": { "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.", "wrong_server_id": "Server ID ass ong\u00eblteg" + }, + "step": { + "user": { + "description": "S\u00e9cher fir SpeedTest anzeriichten?", + "title": "SpeedTest ariichten" + } } }, "options": { + "error": { + "retrive_error": "Feeler beim ofruffen vun der Server L\u00ebscht" + }, "step": { "init": { "data": { + "manual": "Auto Update deaktiv\u00e9ieren", "scan_interval": "Intervalle vun de Mise \u00e0 jour (Minutten)", "server_name": "Test Server auswielen" } diff --git a/homeassistant/components/squeezebox/translations/ca.json b/homeassistant/components/squeezebox/translations/ca.json index 6033d7ae449..3d3ce7f7c08 100644 --- a/homeassistant/components/squeezebox/translations/ca.json +++ b/homeassistant/components/squeezebox/translations/ca.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_server_found": "No s'ha trobat cap servidor LMS." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_server_found": "No s'ha pogut descobrir el servidor autom\u00e0ticament.", "unknown": "Error inesperat" }, "flow_title": "Logitech Squeezebox: {host}", @@ -16,12 +18,14 @@ "password": "Contrasenya", "port": "Port", "username": "Nom d'usuari" - } + }, + "title": "Edita la informaci\u00f3 de connexi\u00f3" }, "user": { "data": { "host": "Amfitri\u00f3" - } + }, + "title": "Configura el servidor Logitech Media" } } }, diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json index 52e4bbe4861..f9f1723575e 100644 --- a/homeassistant/components/toon/translations/ca.json +++ b/homeassistant/components/toon/translations/ca.json @@ -16,6 +16,13 @@ "display_exists": "La pantalla seleccionada ja est\u00e0 configurada." }, "step": { + "agreement": { + "data": { + "agreement": "Acord" + }, + "description": "Seleccioneu l'adre\u00e7a de l'acord a afegir.", + "title": "Selecciona acord" + }, "authenticate": { "data": { "password": "Contrasenya", @@ -31,6 +38,9 @@ }, "description": "Selecciona la pantalla Toon amb la qual vols connectar-te.", "title": "Selecci\u00f3 de pantalla" + }, + "pick_implementation": { + "title": "Tria amb quin vols autenticar-te" } } } diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index e2b014ac019..7e319ca1cfd 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -12,7 +12,9 @@ "user": { "data": { "host": "Amfitri\u00f3", + "limit": "L\u00edmit", "name": "Nom", + "order": "Ordre", "password": "Contrasenya", "port": "Port", "username": "Nom d'usuari" @@ -25,6 +27,8 @@ "step": { "init": { "data": { + "limit": "L\u00edmit", + "order": "Ordre", "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" }, "title": "Opcions de configuraci\u00f3 de Transmission" diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 886a5afc773..a760cb65ac7 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -27,6 +27,8 @@ "step": { "init": { "data": { + "limit": "L\u00edmite", + "order": "Pedido", "scan_interval": "Frecuencia de actualizaci\u00f3n" }, "title": "Configurar opciones para la transmisi\u00f3n" diff --git a/homeassistant/components/transmission/translations/lb.json b/homeassistant/components/transmission/translations/lb.json index a98c41578fa..eb305ea7980 100644 --- a/homeassistant/components/transmission/translations/lb.json +++ b/homeassistant/components/transmission/translations/lb.json @@ -12,7 +12,9 @@ "user": { "data": { "host": "Host", + "limit": "Limite", "name": "Numm", + "order": "Reiefolleg", "password": "Passwuert", "port": "Port", "username": "Benotzernumm" @@ -25,6 +27,8 @@ "step": { "init": { "data": { + "limit": "Limite", + "order": "Reiefolleg", "scan_interval": "Intervalle vun de Mise \u00e0 jour" }, "title": "Optioune fir Transmission konfigur\u00e9ieren" diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index 0a850c68f17..c51d32733a8 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -27,6 +27,8 @@ "step": { "init": { "data": { + "limit": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435", + "order": "\u041f\u043e\u0440\u044f\u0434\u043e\u043a", "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Transmission" diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index b8b30ab4159..3d343ef2e3e 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -12,7 +12,9 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", + "limit": "\u9650\u5236", "name": "\u540d\u7a31", + "order": "\u6392\u5e8f", "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" @@ -25,6 +27,8 @@ "step": { "init": { "data": { + "limit": "\u9650\u5236", + "order": "\u6392\u5e8f", "scan_interval": "\u66f4\u65b0\u983b\u7387" }, "title": "Transmission \u8a2d\u5b9a\u9078\u9805" diff --git a/homeassistant/components/xiaomi_aqara/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json index c3422a85efb..f3c9b24137d 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ca.json +++ b/homeassistant/components/xiaomi_aqara/translations/ca.json @@ -1,24 +1,38 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 de la passarel\u00b7la ja est\u00e0 en curs.", + "not_xiaomi_aqara": "No \u00e9s una passarel\u00b7la Xiaomi Aqara, el dispositiu descobert no coincideix amb cap passarel\u00b7la coneguda" }, "error": { + "discovery_error": "No s'ha pogut descobrir cap passarel\u00b7la Xiaomi Aqara, prova d'utilitzar la IP del dispositiu que executa Home Assistant com a interf\u00edcie", "invalid_interface": "Interf\u00edcie de xarxa no v\u00e0lida", - "invalid_key": "Clau de la passarel\u00b7la no v\u00e0lida" + "invalid_key": "Clau de la passarel\u00b7la no v\u00e0lida", + "not_found_error": "No s'ha pogut descobrir cap passarel\u00b7la Zeroconf per obtenir informaci\u00f3 necess\u00e0ria, prova d'utilitzar la IP del dispositiu que executa Home Assistant com a interf\u00edcie" }, "flow_title": "Passarel\u00b7la Xiaomi Aqara: {name}", "step": { "select": { "data": { "select_ip": "IP de la passarel\u00b7la" - } + }, + "description": "Torna a executar la configuraci\u00f3 si vols connectar passarel\u00b7les addicionals", + "title": "Selecciona la passarel\u00b7la Xiaomi Aqara a la qual connectar-te" }, "settings": { "data": { "key": "Clau de la passarel\u00b7la", "name": "Nom de la passarel\u00b7la" - } + }, + "title": "Passarel\u00b7la Xiaomi Aqara, configuraci\u00f3 opcional" + }, + "user": { + "data": { + "interface": "Interf\u00edcie de xarxa a utilitzar" + }, + "description": "Connecta't a la passarel\u00b7la Xiaomi Aqara", + "title": "Passarel\u00b7la Xiaomi Aqara" } } } From 856f8fd6deae956fdea53616acd39c3f80d783d1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 29 Jun 2020 18:24:42 -0600 Subject: [PATCH 067/428] Move Guardian services to entity platform services (#37189) --- homeassistant/components/guardian/__init__.py | 94 +------------------ homeassistant/components/guardian/switch.py | 82 ++++++++++++++++ 2 files changed, 83 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 7fc12bece26..816c9ccc0f1 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,34 +3,17 @@ import asyncio from datetime import timedelta from aioguardian import Client -from aioguardian.commands.system import ( - DEFAULT_FIRMWARE_UPGRADE_FILENAME, - DEFAULT_FIRMWARE_UPGRADE_PORT, - DEFAULT_FIRMWARE_UPGRADE_URL, -) from aioguardian.errors import GuardianError -import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_FILENAME, - CONF_IP_ADDRESS, - CONF_PORT, - CONF_URL, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_IP_ADDRESS from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.service import ( - async_register_admin_service, - verify_domain_control, -) from .const import ( CONF_UID, @@ -61,16 +44,6 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) PLATFORMS = ["binary_sensor", "sensor", "switch"] -SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_URL, default=DEFAULT_FIRMWARE_UPGRADE_URL): cv.url, - vol.Optional(CONF_PORT, default=DEFAULT_FIRMWARE_UPGRADE_PORT): cv.port, - vol.Optional( - CONF_FILENAME, default=DEFAULT_FIRMWARE_UPGRADE_FILENAME - ): cv.string, - } -) - @callback def async_get_api_category(entity_kind: str): @@ -86,8 +59,6 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Elexa Guardian from a config entry.""" - _verify_domain_control = verify_domain_control(hass, DOMAIN) - guardian = Guardian(hass, entry) await guardian.async_update() hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = guardian @@ -97,69 +68,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, component) ) - @_verify_domain_control - async def disable_ap(call): - """Disable the device's onboard access point.""" - try: - async with guardian.client: - await guardian.client.wifi.disable_ap() - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - @_verify_domain_control - async def enable_ap(call): - """Enable the device's onboard access point.""" - try: - async with guardian.client: - await guardian.client.wifi.enable_ap() - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - @_verify_domain_control - async def reboot(call): - """Reboot the device.""" - try: - async with guardian.client: - await guardian.client.system.reboot() - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - @_verify_domain_control - async def reset_valve_diagnostics(call): - """Fully reset system motor diagnostics.""" - try: - async with guardian.client: - await guardian.client.valve.reset() - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - @_verify_domain_control - async def upgrade_firmware(call): - """Upgrade the device firmware.""" - try: - async with guardian.client: - await guardian.client.system.upgrade_firmware( - url=call.data[CONF_URL], - port=call.data[CONF_PORT], - filename=call.data[CONF_FILENAME], - ) - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - for service, method, schema in [ - ("disable_ap", disable_ap, None), - ("enable_ap", enable_ap, None), - ("reboot", reboot, None), - ("reset_valve_diagnostics", reset_valve_diagnostics, None), - ("upgrade_firmware", upgrade_firmware, SERVICE_UPGRADE_FIRMWARE_SCHEMA), - ]: - async_register_admin_service(hass, DOMAIN, service, method, schema=schema) - return True diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index f8af11768d2..461eeaaeedb 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,8 +1,16 @@ """Switches for the Elexa Guardian integration.""" +from aioguardian.commands.system import ( + DEFAULT_FIRMWARE_UPGRADE_FILENAME, + DEFAULT_FIRMWARE_UPGRADE_PORT, + DEFAULT_FIRMWARE_UPGRADE_URL, +) from aioguardian.errors import GuardianError +import voluptuous as vol from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_FILENAME, CONF_PORT, CONF_URL from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv, entity_platform from . import Guardian, GuardianEntity from .const import DATA_CLIENT, DATA_VALVE_STATUS, DOMAIN, LOGGER, SWITCH_KIND_VALVE @@ -12,10 +20,42 @@ ATTR_INST_CURRENT = "instantaneous_current" ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt" ATTR_TRAVEL_COUNT = "travel_count" +SERVICE_DISABLE_AP = "disable_ap" +SERVICE_ENABLE_AP = "enable_ap" +SERVICE_REBOOT = "reboot" +SERVICE_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" +SERVICE_UPGRADE_FIRMWARE = "upgrade_firmware" + +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_URL, default=DEFAULT_FIRMWARE_UPGRADE_URL): cv.url, + vol.Optional(CONF_PORT, default=DEFAULT_FIRMWARE_UPGRADE_PORT): cv.port, + vol.Optional( + CONF_FILENAME, default=DEFAULT_FIRMWARE_UPGRADE_FILENAME + ): cv.string, + } +) + async def async_setup_entry(hass, entry, async_add_entities): """Set up Guardian switches based on a config entry.""" guardian = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + + platform = entity_platform.current_platform.get() + + for service_name, schema, method in [ + (SERVICE_DISABLE_AP, None, "async_disable_ap"), + (SERVICE_ENABLE_AP, None, "async_enable_ap"), + (SERVICE_REBOOT, None, "async_reboot"), + (SERVICE_RESET_VALVE_DIAGNOSTICS, None, "async_reset_valve_diagnostics"), + ( + SERVICE_UPGRADE_FIRMWARE, + SERVICE_UPGRADE_FIRMWARE_SCHEMA, + "async_upgrade_firmware", + ), + ]: + platform.async_register_entity_service(service_name, schema, method) + async_add_entities([GuardianSwitch(guardian)], True) @@ -60,6 +100,48 @@ class GuardianSwitch(GuardianEntity, SwitchEntity): } ) + async def async_disable_ap(self): + """Disable the device's onboard access point.""" + try: + async with self._guardian.client: + await self._guardian.client.wifi.disable_ap() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + + async def async_enable_ap(self): + """Enable the device's onboard access point.""" + try: + async with self._guardian.client: + await self._guardian.client.wifi.enable_ap() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + + async def async_reboot(self): + """Reboot the device.""" + try: + async with self._guardian.client: + await self._guardian.client.system.reboot() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + + async def async_reset_valve_diagnostics(self): + """Fully reset system motor diagnostics.""" + try: + async with self._guardian.client: + await self._guardian.client.valve.reset() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + + async def async_upgrade_firmware(self, *, url, port, filename): + """Upgrade the device firmware.""" + try: + async with self._guardian.client: + await self._guardian.client.system.upgrade_firmware( + url=url, port=port, filename=filename, + ) + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + async def async_turn_off(self, **kwargs) -> None: """Turn the valve off (closed).""" try: From 0f43476d0347dc3c56d4974e3629a398f82880cf Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 29 Jun 2020 18:25:01 -0600 Subject: [PATCH 068/428] Fix Tile location accuracy bug (#37233) Co-authored-by: Paulus Schoutsen --- .../components/tile/device_tracker.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 910732f7c04..5b0065b2c4e 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -84,13 +84,26 @@ class TileDeviceTracker(TileEntity, TrackerEntity): Value in meters. """ - return round( - ( - self._tile["last_tile_state"]["h_accuracy"] - + self._tile["last_tile_state"]["v_accuracy"] + state = self._tile["last_tile_state"] + h_accuracy = state.get("h_accuracy") + v_accuracy = state.get("v_accuracy") + + if h_accuracy is not None and v_accuracy is not None: + return round( + ( + self._tile["last_tile_state"]["h_accuracy"] + + self._tile["last_tile_state"]["v_accuracy"] + ) + / 2 ) - / 2 - ) + + if h_accuracy is not None: + return h_accuracy + + if v_accuracy is not None: + return v_accuracy + + return None @property def latitude(self) -> float: From fae47358b84f5cd44c6a51696ed9a2b5c4bfb09b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jun 2020 19:34:38 -0500 Subject: [PATCH 069/428] Use shared zeroconf for discovery netdisco (#37237) * Use shared zeroconf for netdisco * Update netdisco Co-authored-by: Paulus Schoutsen --- homeassistant/components/discovery/__init__.py | 9 ++++++--- homeassistant/components/discovery/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/discovery/test_init.py | 12 ++++++------ 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 1c2f816ad40..c39cb62c34c 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -14,6 +14,7 @@ from netdisco.discovery import NetworkDiscovery import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -147,6 +148,8 @@ async def async_setup(hass, config): platform, ) + zeroconf_instance = await zeroconf.async_get_instance(hass) + async def new_service_found(service, info): """Handle a new service if one is found.""" if service in MIGRATED_SERVICE_HANDLERS: @@ -193,7 +196,7 @@ async def async_setup(hass, config): async def scan_devices(now): """Scan for devices.""" try: - results = await hass.async_add_job(_discover, netdisco) + results = await hass.async_add_job(_discover, netdisco, zeroconf_instance) for result in results: hass.async_create_task(new_service_found(*result)) @@ -214,11 +217,11 @@ async def async_setup(hass, config): return True -def _discover(netdisco): +def _discover(netdisco, zeroconf_instance): """Discover devices.""" results = [] try: - netdisco.scan() + netdisco.scan(zeroconf_instance=zeroconf_instance) for disc in netdisco.discover(): for service in netdisco.get_info(disc): diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 4b716b604f1..962ba9b8e8c 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,7 +2,7 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": ["netdisco==2.7.1"], + "requirements": ["netdisco==2.8.0"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index d73bae27bb2..3dde2e9002e 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["defusedxml==0.6.0", "netdisco==2.7.1"], + "requirements": ["defusedxml==0.6.0", "netdisco==2.8.0"], "after_dependencies": ["zeroconf"], "codeowners": [] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 70957391911..0a2f0a5d090 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ hass-nabucasa==0.34.7 home-assistant-frontend==20200629.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 -netdisco==2.7.1 +netdisco==2.8.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2020.1 diff --git a/requirements_all.txt b/requirements_all.txt index 6eab3d3658e..d0ba8d8c7b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -953,7 +953,7 @@ netdata==0.2.0 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.7.1 +netdisco==2.8.0 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a62951f422f..f4876770bdc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -424,7 +424,7 @@ nessclient==0.9.15 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.7.1 +netdisco==2.8.0 # homeassistant.components.nexia nexia==0.9.3 diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 9490707e0f6..da04793e2a9 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -58,7 +58,7 @@ async def mock_discovery(hass, discoveries, config=BASE_CONFIG): async def test_unknown_service(hass): """Test that unknown service is ignored.""" - def discover(netdisco): + def discover(netdisco, zeroconf_instance): """Fake discovery.""" return [("this_service_will_never_be_supported", {"info": "some"})] @@ -71,7 +71,7 @@ async def test_unknown_service(hass): async def test_load_platform(hass): """Test load a platform.""" - def discover(netdisco): + def discover(netdisco, zeroconf_instance): """Fake discovery.""" return [(SERVICE, SERVICE_INFO)] @@ -87,7 +87,7 @@ async def test_load_platform(hass): async def test_load_component(hass): """Test load a component.""" - def discover(netdisco): + def discover(netdisco, zeroconf_instance): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] @@ -107,7 +107,7 @@ async def test_load_component(hass): async def test_ignore_service(hass): """Test ignore service.""" - def discover(netdisco): + def discover(netdisco, zeroconf_instance): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] @@ -120,7 +120,7 @@ async def test_ignore_service(hass): async def test_discover_duplicates(hass): """Test load a component.""" - def discover(netdisco): + def discover(netdisco, zeroconf_instance): """Fake discovery.""" return [ (SERVICE_NO_PLATFORM, SERVICE_INFO), @@ -145,7 +145,7 @@ async def test_discover_config_flow(hass): """Test discovery triggering a config flow.""" discovery_info = {"hello": "world"} - def discover(netdisco): + def discover(netdisco, zeroconf_instance): """Fake discovery.""" return [("mock-service", discovery_info)] From 2f46a81e3ebddadb501f8c2a796271e543d20648 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Tue, 30 Jun 2020 02:49:22 +0200 Subject: [PATCH 070/428] Update fritzconnection to 1.3.0 (#37212) This effectively fixes an important bug where the graph would go negative because 1.2.0 used the 32-bit counters and 1.3.0 uses 64-bit counters will not realistically go negative any time soon. --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- homeassistant/components/fritzbox_netmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 3723bd7885a..b82038b8404 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,6 +2,6 @@ "domain": "fritz", "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.2.0"], + "requirements": ["fritzconnection==1.3.0"], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index b5fa26c096b..7f78b0ab9b5 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "fritzbox_callmonitor", "name": "AVM FRITZ!Box Call Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.2.0"], + "requirements": ["fritzconnection==1.3.0"], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index dde4d634867..4813c98442e 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "fritzbox_netmonitor", "name": "AVM FRITZ!Box Net Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", - "requirements": ["fritzconnection==1.2.0"], + "requirements": ["fritzconnection==1.3.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index d0ba8d8c7b3..9c47005087e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ freesms==0.1.2 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -fritzconnection==1.2.0 +fritzconnection==1.3.0 # homeassistant.components.google_translate gTTS-token==1.1.3 From 4d17b18761ac6569317d283f0632cee07851a92f Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 29 Jun 2020 18:17:04 -0700 Subject: [PATCH 071/428] Register 'androidtv.learn_sendevent' service (#35707) --- .../components/androidtv/media_player.py | 112 ++++++++++-------- .../components/androidtv/services.yaml | 6 + .../components/androidtv/test_media_player.py | 29 +++++ 3 files changed, 96 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 4fd7b70835d..311a8b7d6c6 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -44,7 +44,7 @@ from homeassistant.const import ( STATE_STANDBY, ) from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.storage import STORAGE_DIR ANDROIDTV_DOMAIN = "androidtv" @@ -103,6 +103,7 @@ DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] SERVICE_ADB_COMMAND = "adb_command" SERVICE_DOWNLOAD = "download" +SERVICE_LEARN_SENDEVENT = "learn_sendevent" SERVICE_UPLOAD = "upload" SERVICE_ADB_COMMAND_SCHEMA = vol.Schema( @@ -117,6 +118,10 @@ SERVICE_DOWNLOAD_SCHEMA = vol.Schema( } ) +SERVICE_LEARN_SENDEVENT_SCHEMA = vol.Schema( + {vol.Required(ATTR_ENTITY_ID): cv.entity_ids} +) + SERVICE_UPLOAD_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_ids, @@ -161,7 +166,36 @@ ANDROIDTV_STATES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_androidtv(hass, config): + """Generate an ADB key (if needed) and connect to the Android TV / Fire TV.""" + adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) + if CONF_ADB_SERVER_IP not in config: + # Use "adb_shell" (Python ADB implementation) + if not os.path.isfile(adbkey): + # Generate ADB key files + keygen(adbkey) + + adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" + + else: + # Use "pure-python-adb" (communicate with ADB server) + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" + + aftv = setup( + config[CONF_HOST], + config[CONF_PORT], + adbkey, + config.get(CONF_ADB_SERVER_IP, ""), + config[CONF_ADB_SERVER_PORT], + config[CONF_STATE_DETECTION_RULES], + config[CONF_DEVICE_CLASS], + 10.0, + ) + + return aftv, adb_log + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Android TV / Fire TV platform.""" hass.data.setdefault(ANDROIDTV_DOMAIN, {}) @@ -171,51 +205,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.warning("Platform already setup on %s, skipping", address) return - if CONF_ADB_SERVER_IP not in config: - # Use "adb_shell" (Python ADB implementation) - if CONF_ADBKEY not in config: - # Generate ADB key files (if they don't exist) - adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey") - if not os.path.isfile(adbkey): - keygen(adbkey) - - adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" - - aftv = setup( - config[CONF_HOST], - config[CONF_PORT], - adbkey, - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - auth_timeout_s=10.0, - ) - - else: - adb_log = ( - f"using Python ADB implementation with adbkey='{config[CONF_ADBKEY]}'" - ) - - aftv = setup( - config[CONF_HOST], - config[CONF_PORT], - config[CONF_ADBKEY], - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - auth_timeout_s=10.0, - ) - - else: - # Use "pure-python-adb" (communicate with ADB server) - adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" - - aftv = setup( - config[CONF_HOST], - config[CONF_PORT], - adb_server_ip=config[CONF_ADB_SERVER_IP], - adb_server_port=config[CONF_ADB_SERVER_PORT], - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - ) + aftv, adb_log = await hass.async_add_executor_job(setup_androidtv, hass, config) if not aftv.available: # Determine the name that will be used for the device in the log @@ -251,13 +241,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device = FireTVDevice(*device_args) device_name = config.get(CONF_NAME, "Fire TV") - add_entities([device]) + async_add_entities([device]) _LOGGER.debug("Setup %s at %s %s", device_name, address, adb_log) hass.data[ANDROIDTV_DOMAIN][address] = device if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): return + platform = entity_platform.current_platform.get() + def service_adb_command(service): """Dispatch service calls to target entities.""" cmd = service.data[ATTR_COMMAND] @@ -280,13 +272,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): output, ) - hass.services.register( + hass.services.async_register( ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND, service_adb_command, schema=SERVICE_ADB_COMMAND_SCHEMA, ) + platform.async_register_entity_service( + SERVICE_LEARN_SENDEVENT, SERVICE_LEARN_SENDEVENT_SCHEMA, "learn_sendevent", + ) + def service_download(service): """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" local_path = service.data[ATTR_LOCAL_PATH] @@ -304,7 +300,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): target_device.adb_pull(local_path, device_path) - hass.services.register( + hass.services.async_register( ANDROIDTV_DOMAIN, SERVICE_DOWNLOAD, service_download, @@ -329,7 +325,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for target_device in target_devices: target_device.adb_push(local_path, device_path) - hass.services.register( + hass.services.async_register( ANDROIDTV_DOMAIN, SERVICE_UPLOAD, service_upload, schema=SERVICE_UPLOAD_SCHEMA ) @@ -587,6 +583,20 @@ class ADBDevice(MediaPlayerEntity): self.schedule_update_ha_state() return self._adb_response + @adb_decorator() + def learn_sendevent(self): + """Translate a key press on a remote to ADB 'sendevent' commands.""" + output = self.aftv.learn_sendevent() + if output: + self._adb_response = output + self.schedule_update_ha_state() + + msg = f"Output from service '{SERVICE_LEARN_SENDEVENT}' from {self.entity_id}: '{output}'" + self.hass.components.persistent_notification.async_create( + msg, title="Android TV" + ) + _LOGGER.info("%s", msg) + @adb_decorator() def adb_pull(self, local_path, device_path): """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" diff --git a/homeassistant/components/androidtv/services.yaml b/homeassistant/components/androidtv/services.yaml index f5efe233271..65e83dfbe4f 100644 --- a/homeassistant/components/androidtv/services.yaml +++ b/homeassistant/components/androidtv/services.yaml @@ -33,3 +33,9 @@ upload: local_path: description: The filepath on your Home Assistant instance. example: "/config/www/example.txt" +learn_sendevent: + description: Translate a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service. + fields: + entity_id: + description: Name(s) of Android TV / Fire TV entities. + example: "media_player.android_tv_living_room" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index d1723c2d6fa..ae311e85229 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -16,6 +16,7 @@ from homeassistant.components.androidtv.media_player import ( KEYS, SERVICE_ADB_COMMAND, SERVICE_DOWNLOAD, + SERVICE_LEARN_SENDEVENT, SERVICE_UPLOAD, ) from homeassistant.components.media_player.const import ( @@ -850,6 +851,34 @@ async def test_adb_command_get_properties(hass): assert state.attributes["adb_response"] == str(response) +async def test_learn_sendevent(hass): + """Test the `androidtv.learn_sendevent` service.""" + patch_key = "server" + entity_id = "media_player.android_tv" + response = "sendevent 1 2 3 4" + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() + + with patch( + "androidtv.basetv.BaseTV.learn_sendevent", return_value=response + ) as patch_learn_sendevent: + await hass.services.async_call( + ANDROIDTV_DOMAIN, + SERVICE_LEARN_SENDEVENT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + patch_learn_sendevent.assert_called() + state = hass.states.get(entity_id) + assert state is not None + assert state.attributes["adb_response"] == response + + async def test_update_lock_not_acquired(hass): """Test that the state does not get updated when a `LockNotAcquiredException` is raised.""" patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) From 61475d0a0ccd1746d1773a8c5042b377302d16d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Tue, 30 Jun 2020 13:02:30 +0200 Subject: [PATCH 072/428] Add support for window covers to ozw integration (#37217) * feat: add cover to ozw * fix: imports * fix: formatting * fix: improve code regarding comments * add: cover tests * fix: add position converting tests * fix: convert cover position form zwave value * fix: improve naming * fix: increase coverage --- homeassistant/components/ozw/const.py | 2 + homeassistant/components/ozw/cover.py | 76 ++++++++++++ homeassistant/components/ozw/discovery.py | 29 +++++ tests/components/ozw/conftest.py | 17 +++ tests/components/ozw/test_cover.py | 86 ++++++++++++++ tests/fixtures/ozw/cover.json | 25 ++++ tests/fixtures/ozw/cover_network_dump.csv | 134 ++++++++++++++++++++++ 7 files changed, 369 insertions(+) create mode 100644 homeassistant/components/ozw/cover.py create mode 100644 tests/components/ozw/test_cover.py create mode 100644 tests/fixtures/ozw/cover.json create mode 100644 tests/fixtures/ozw/cover_network_dump.csv diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index 8115b18a0e8..8b18ce707c5 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -1,6 +1,7 @@ """Constants for the ozw integration.""" from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN @@ -11,6 +12,7 @@ DOMAIN = "ozw" DATA_UNSUBSCRIBE = "unsubscribe" PLATFORMS = [ BINARY_SENSOR_DOMAIN, + COVER_DOMAIN, CLIMATE_DOMAIN, FAN_DOMAIN, LIGHT_DOMAIN, diff --git a/homeassistant/components/ozw/cover.py b/homeassistant/components/ozw/cover.py new file mode 100644 index 00000000000..900debb35ac --- /dev/null +++ b/homeassistant/components/ozw/cover.py @@ -0,0 +1,76 @@ +"""Support for Z-Wave cover devices.""" +import logging + +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +SUPPORTED_FEATURES_POSITION = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Cover from Config Entry.""" + + @callback + def async_add_cover(values): + """Add Z-Wave Cover.""" + + async_add_entities([ZWaveCoverEntity(values)]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect(hass, f"{DOMAIN}_new_{COVER_DOMAIN}", async_add_cover) + ) + + +def percent_to_zwave_position(value): + """Convert position in 0-100 scale to 0-99 scale. + + `value` -- (int) Position byte value from 0-100. + """ + if value > 0: + return max(1, round((value / 100) * 99)) + return 0 + + +class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity): + """Representation of a Z-Wave Cover device.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES_POSITION + + @property + def is_closed(self): + """Return true if cover is closed.""" + return self.values.primary.value == 0 + + @property + def current_cover_position(self): + """Return the current position of cover where 0 means closed and 100 is fully open.""" + return round((self.values.primary.value / 99) * 100) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + self.values.primary.send_value(percent_to_zwave_position(kwargs[ATTR_POSITION])) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self.values.primary.send_value(99) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + self.values.primary.send_value(0) diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py index f9b0bdb3551..3b3edf1c118 100644 --- a/homeassistant/components/ozw/discovery.py +++ b/homeassistant/components/ozw/discovery.py @@ -131,6 +131,35 @@ DISCOVERY_SCHEMAS = ( }, }, }, + { # Rollershutter + const.DISC_COMPONENT: "cover", + const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,), + const.DISC_SPECIFIC_DEVICE_CLASS: ( + const_ozw.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, + const_ozw.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, + const_ozw.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, + const_ozw.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, + const_ozw.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, + const_ozw.SPECIFIC_TYPE_SECURE_DOOR, + ), + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, + const.DISC_GENRE: ValueGenre.USER, + }, + "open": { + const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_BRIGHT, + const.DISC_OPTIONAL: True, + }, + "close": { + const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DIM, + const.DISC_OPTIONAL: True, + }, + }, + }, { # Fan const.DISC_COMPONENT: "fan", const.DISC_GENERIC_DEVICE_CLASS: const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index 5f29435760c..0d1522980d1 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -27,6 +27,12 @@ def light_data_fixture(): return load_fixture("ozw/light_network_dump.csv") +@pytest.fixture(name="cover_data", scope="session") +def cover_data_fixture(): + """Load cover MQTT data and return it.""" + return load_fixture("ozw/cover_network_dump.csv") + + @pytest.fixture(name="climate_data", scope="session") def climate_data_fixture(): """Load climate MQTT data and return it.""" @@ -119,6 +125,17 @@ async def binary_sensor_alt_msg_fixture(hass): return message +@pytest.fixture(name="cover_msg") +async def cover_msg_fixture(hass): + """Return a mock MQTT msg with a cover level change message.""" + sensor_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/cover.json") + ) + message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) + message.encode() + return message + + @pytest.fixture(name="climate_msg") async def climate_msg_fixture(hass): """Return a mock MQTT msg with a climate mode change message.""" diff --git a/tests/components/ozw/test_cover.py b/tests/components/ozw/test_cover.py new file mode 100644 index 00000000000..fa1c709e147 --- /dev/null +++ b/tests/components/ozw/test_cover.py @@ -0,0 +1,86 @@ +"""Test Z-Wave Covers.""" +from homeassistant.components.cover import ATTR_CURRENT_POSITION + +from .common import setup_ozw + + +async def test_cover(hass, cover_data, sent_messages, cover_msg): + """Test setting up config entry.""" + receive_message = await setup_ozw(hass, fixture=cover_data) + # Test loaded + state = hass.states.get("cover.roller_shutter_3_instance_1_level") + assert state is not None + assert state.state == "closed" + assert state.attributes[ATTR_CURRENT_POSITION] == 0 + + # Test opening + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": "cover.roller_shutter_3_instance_1_level"}, + blocking=True, + ) + assert len(sent_messages) == 1 + msg = sent_messages[0] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905} + + # Feedback on state + cover_msg.decode() + cover_msg.payload["Value"] = 99 + cover_msg.encode() + receive_message(cover_msg) + await hass.async_block_till_done() + + state = hass.states.get("cover.roller_shutter_3_instance_1_level") + assert state is not None + assert state.state == "open" + assert state.attributes[ATTR_CURRENT_POSITION] == 100 + + # Test closing + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": "cover.roller_shutter_3_instance_1_level"}, + blocking=True, + ) + assert len(sent_messages) == 2 + msg = sent_messages[1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905} + + # Test setting position + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 50}, + blocking=True, + ) + assert len(sent_messages) == 3 + msg = sent_messages[2] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 50, "ValueIDKey": 625573905} + + # Test converting position to zwave range for position > 0 + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 100}, + blocking=True, + ) + assert len(sent_messages) == 4 + msg = sent_messages[3] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905} + + # Test converting position to zwave range for position = 0 + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 0}, + blocking=True, + ) + assert len(sent_messages) == 5 + msg = sent_messages[4] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905} diff --git a/tests/fixtures/ozw/cover.json b/tests/fixtures/ozw/cover.json new file mode 100644 index 00000000000..ece62617edd --- /dev/null +++ b/tests/fixtures/ozw/cover.json @@ -0,0 +1,25 @@ +{ + "topic": "OpenZWave/1/node/37/instance/1/commandclass/38/value/625573905/", + "payload": { + "Label": "Instance 1: Level", + "Value": 0, + "Units": "", + "ValueSet": true, + "ValuePolled": false, + "ChangeVerified": false, + "Min": 0, + "Max": 255, + "Type": "Byte", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", + "Index": 0, + "Node": 37, + "Genre": "User", + "Help": "The Current Level of the Device", + "ValueIDKey": 625573905, + "ReadOnly": false, + "WriteOnly": false, + "Event": "valueChanged", + "TimeStamp": 1593370642 + } +} \ No newline at end of file diff --git a/tests/fixtures/ozw/cover_network_dump.csv b/tests/fixtures/ozw/cover_network_dump.csv new file mode 100644 index 00000000000..6c469361ed5 --- /dev/null +++ b/tests/fixtures/ozw/cover_network_dump.csv @@ -0,0 +1,134 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1173", "OZWDaemon_Version": "0.1.149", "QTOpenZWave_Version": "1.2.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1593370382, "ManufacturerSpecificDBReady": true, "homeID": 3716538409, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 4.61", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/ttyACM0"} +OpenZWave/1/node/37/,{ "NodeID": 37, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0303", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgr223.png", "Description": "FIBARO Roller Shutter 3 is a device designed to control roller blinds, awnings, venetian blinds, gates and other single phase, AC powered devices. Roller Shutter 3 allows precise positioning of roller blinds or venetian blind lamellas. The device is equipped with power and energy monitoring. It allows to control connected devices either via the Z-Wave network or via a switch connected directly to it. Main features of FIBARO Roller Shutter 3: - Compatible with any Z-Wave or Z-Wave Plus Controller, - Supports Z-Wave network Security Modes: S0 with AES-128 encryption and S2 with PRNG-based encryption, - To be installed with roller blind motors with electronic or mechanical limit switches, - Advanced microprocessor control, - Active power and energy metering functionality, - Works with various types of switches – momentary, toggle and dedicated roller blind switches, - To be installed in wall switch boxes, - Works as a Z-Wave signal repeater.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/3278/FGR-223-EN-T-v1.3.pdf", "ProductPageURL": "", "InclusionHelp": "To add the device to the Z-Wave network manually: 1. Power the device. 2. Identify the S1 switch. 3. Set the main controller in (Security/non-Security Mode) add mode (see the controller’s manual). 4. Quickly, triple click the S1 switch. 5. If you are adding in Security S2, scan the DSK QR code or input the underlined part of the DSK (label on the bottom of the box). 6. Wait for the adding process to end. 7. Successful adding will be confirmed by the Z-Wave controller’s message. To add the device to the Z-Wave network using Smart Start: 1. Set the main controller in Security S2 Authenticated add mode (see the controller’s manual) 2. Scan the DSK QR code or input the underlined part of the DSK 3. (label on the bottom of the box). 4. Power the device (turn on the mains voltage). 5. LED will start blinking yellow, wait for the adding process to end. 6. Successful adding will be confirmed by the Z-Wave controller’s message.", "ExclusionHelp": "To remove the device from the Z-Wave network: 1. Make sure the device is powered. 2. Identify the S1 switch. 3. Set the main controller in remove mode (see the controller’s manual). 4. Quickly, triple click the S1 switch. 5. Wait for the removing process to end. 6. Successful removing will be confirmed by the Z-Wave controller’s message.", "ResetHelp": "Reset procedure allows to restore the device back to its factory settings, which means all information about the Z-Wave controller and user configuration will be deleted. 1. Switch off the mains voltage (disable the fuse). 2. Remove the device from the wall switch box. 3. Switch on the mains voltage. 4. Press and hold the B-button to enter the menu. 5. Wait for the LED indicator to glow yellow. 6. Quickly release and click the B-button again. 7. After few seconds the device will be restarted, which is signalled with the red LED indicator colour. Please use this procedure only when the network primary controller is missing or otherwise inoperable.\"", "WakeupHelp": "FIBARO Roller Shutter 3 is powered with mains voltage so it is always awake.", "ProductSupportURL": "", "Frequency": "", "Name": "Roller Shutter 3", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO19eZgcxX12Vd89PffMzszeqz20uoVOBEjiBhkEPnFsbGMDPvBF/CUccUyIHwdsII5JbIIdbMfBBidgA+YyCAwIS5ySWN3aXaTVald7787O2Xd3fX+UtmnNpdljwFH6fR7p6e3prq6uevt31a+qIEIIOHAw1yA+6Ao4OD3hEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FF4BDLQUXgEMtBReAQy0FFQH3QFXBQGKX36oYQvm81mRkcYr1PKEYUhBD+qdiBdQwhpCgKAACnYB3bC/wL4ZxDrJmghDjJJwcowpXS5ZQuvOCvOWz7YDnnEOvUvVuQJXZylCAKljT2g3KeOAPYq5HzCHgyCILIZ9iccw5W4iU/QJTzOqcUKuDkfrKYgY9z+mxaDTib1sacsFepYMkFH5HPuYJssz9iljjdJBZCyDAMUMRkmXGZBY9PWWYOBWf2dHtRBXlc4qeCsAtO0zTtMpWiKI7jZlNPC6cVsTCrVFX9SzBg8yk441rNUquUlmF2eTyHOA3jWB84q4oJqpl1XmlanNIPKChupyV3Z4bTkFgVshrLUYjF9BSYkqbTqpml44rVoeCbWjqx2AUFbylY+dngdCNWjuM2Xd8eIQSKXAUhFEURIaTruq7r9qcAADRNQwiNj4/newAIoWQiceXmK9atXbt927ZTmmXF2FNCmeaUWUJm51uc5fNvWjjdiFUQxVpz7969r23f3tXZaTFg/759Tz311MsvvZxj+yOE/vH22zees/6qj338n++55+WXX87pjMs2bUqlUldcfjlBEJqmpZJJhFAikejt7TUM4+mnnzn/ggvu/P73n3ziD6U1tSU8SmvAnD9noP2xYKuQdAcAkN/97ncrVPQHAtM0sacDTtXcBEFcefnmVCp9/33/vnnzZo7jEEKvvPzy1le2iqLYvmBBd3e32+0+evTokSNHqqqqbr/tH954682+vv7x8fGB48efffoZ3uXatXOnidB3/u7bXZ1df3711cGBQVVR77n77t/8+jfBQPCL113/3PPPffKTn1x+xvLa2tof3Hnn5iuuWLBgQWmJUrrO9j9Lv2AJCVcMJEni4P7scVp5hRbKbFCvz/dvP/nxl7/4xb379v3bvfcCCK+44goAQFYUb/jyVzweT9v8+U/94Q9f/+Y3165d29LSsv6ss6+48spwOKSqWjKRePz3j8VisWgsFo3FqmtqHnzoN5suuSRcFW5oqK+prXv00UdUVd3ywgs0TWcyGbfbfeFFFz/x+OMf/dhHAQDpdLqjowP3Pa5tQ0NDQ0NDsaoiW1Ag/wXL1fKFWqlCQuv0JFY+7P1hmbfxiYmLL7xIUZQD+/bfdMstBw8ceP21171e7969e9PpFMPQO3fs2LBxw2c/+xlJlm/5u1tlWf7ed79L08wNX/3qwQMHuro6EUKmYeBHmKaJEJAkiWW5xUsWNzY29vf10wwDEPrJj38cCARaWlte3boVX+zxeDZu3JhTvdLIqXyZ15dukJzjOSTZaUisfBWAEMpms/v27cPiwTTNM844w+VyBYPBF1/607Vf+EJff/+SpUskSWJZFgHk8bjr6+quu/767du3Dw0NAggVRbnhy1+55dZbICT8AT8uNRAIbt+2ra+vLxgMCoLw6/96UNf10dHR3bt39xzt+cQnPgEJAgKAAPjs5z73peuvV2Tln3/0L/my55TmvP0gx1gsUzYXFFfYxrKXMIeRmtNqSAchpGlafoDULq6s9yUI4kOXbvrj88/dfdddS5Yu/Y+f/qyqquqjH/tod3d3TXVNT09PT0/Pl77y5eP9/R/7+MchhK9u3bply5azzjq7pramqqoqlUp5vd6Od95pbGxyCS63251Opb0+L0EQJEmK2Wx9Q4OiKDiQjQCC029qO3swA0iSLHFlwbfOf2iJMxRF8Tw/rUoWw/8JYhUDhNBEJgQQAAABRODEF2yXJRBCBAAsW03kDyaCMkZ+yrSZyresi7EKnCwC7bVFCNE0PVfEOg1VIUY5oR2EEGYVAADHr/LzTE70UJGn5EcH7PZ4mf58OaxCCJU5PFziiQXbxP4hlVN+mTg9iZVjvkyryezhq5zz9gQYDCvKBU4WTqUDCmXGOYtVrFj5ZdpqxU7Ore46DYlll/A50j5fNhTsIdM086+xyySLW/ikaZplSp38OhS8pfwQVJkewPuP05BYGHY5b08Uybksx0DOOTZN0zR1gqByUk2wbiQIApzQpFhoEQUJYZ3JH/UrZoGVw6ryhc0pZVUlcLoRyy6o8Jlpffp2WQVOZMMBABD2chAyETIhJBFCpmkghAUVBAASBIEvsEbJLEsr/ynFiJ7DttkHMD9ASXa6Easc5MgVS7XlW0uqKoly1iP4cUDKMHTTNCE0cL7liYxCgBAyGZrDxILQxBJuBsZQDqVK1LmcX6dlzM05/05bYuX7/PY/c/QIVnm6rhumQRCQIhmCICYmhxPJSUVTwkHJxbt51qXpmqYrhq4TBOkWvCRJGIYpyyJJkiRBKZpsmiZDMRQFAKDsRtgM6lz6z1P6vDnhhnxTrLSFMHuchsQq9qUW/EynNKAhSpmR8UFFkQFCHC8Ypj45OaGZum7oifRkKBCuqaoDAMqqFJ8cS4vp9uZFLs6jakomm3S7valsIpWa5DieFPwEQQIA8P8FFWKxauODgnQsXy1alBofHw8Gg3944g/4+Rs2bhRcLm4qTDW3wYV8/MV5E7MBQkhVVUVRcrohR/FZf+I8CISQpinxxGjXkQMTiQnD1E3TYBiGYViapgmSqo3UhfxhwzQRMBOpRN/gUZbhQoEqlmaTqYQguEUxI8pSNFwdC1XTNEORDACAoiiSpAtOiSlY83JesHSA1CpkdGT0S1/84rvd3X94+qlLL7yIIAgEwMP//dtoNNY0r6nYjQghhmGcAGkB5Ks86zgnWICPNU2R5CxBkAiZqXRCUqVIJNo3cMQwDU1RIAVYmjUN5WBPh88bNEwtI2ZcnKvz2AGCoFLptMflRggKnGciMel1uaOh2ODYgMALsXDddAVMaaWZbxQWfDULe/buOWf9+s9/4fP79++3LgJgyoU9uZwKhbJOK2LZkW8721sN6750JiErIgAAQCItpSmalJQsxVAsRXMcT1GUhrIZJZmQxwfTPSYykIl4RkjpEz2DPSSk2SSXymQmJlOGbp7RunRwvB8CwucJmKYRDdcSJJFvwhezjSwHotiLFPyzmA1OUVRbW2sgGEynUgzL3vLtv3vsd78rSJnK6av3lVj4NZLJ5Pj4uCRJPM9HIhGPxzNX+j7HVgW2j9s+xoKQKcvi4aOHKJIMh6ITk6NpKeP3BnoGDxqmBkgDEUAGiq4pkpoS1bSEMgzLiloqLo8LwD1hxoE7reqQpj08CYMM63eFXR7m6FBn0BOlKCgqWZplw74IBIAkmRLSqISHYUdONA4UJwS+Zs2aNY8//vixY32f/KtPBgKBV7duPXTw0LRacvZ4n4iFEBoYGHj99dd37Nhx+PDhVCqlKAoAwO/3L1my5JJLLjnnnHMYhpkThpUQEjj+ZJoGSVKaqiQV0YSw93g3zdAD40cyyqQGFIS0eLqfpAyGIyiGVNk05SIMaGQyw5IiZSQaEYARkKabNC+HuRDS3BzhdtEsYDREqbIuul0BluGyUsZEBs/wFMlQFMUwPDiVUst5hRyU3zhut/uaa67BFqeu6x/76MfGx8bBdOTi7FFxYiGEjh49+tRTT7355ptjY2Otra2XXnrpvHnzPB5PMpncu3fv1q1bX3nlldWrV99www2LFy+ezUzccoKipmkYpmHoGstxaSlNkpCkCc2UEKkjWsuow4OJg7wHcQIkKC6RNhQRTgybw8PKZEo2TQiAbugAIUAQyMtrVX6J5xiPR5JdsgaTKkwbsC4YCMVTw6qmKYoCTNTSsKjv6JHG2taAL4wjqCWiXMV0Zekr8weU3n777TPPPBMAkMlk7rjjnyAkAAAul6vgUyqBChILIZRKpR577LGXXnppZGRk8+bNn/70pyORiP2aTZs23XzzzVu3bv3hD3/493//99dee+1HPvKR8l306dYH4D5AAEKysa5VR/rxkV5JTSNSM0mFdhkaHHbzeiqtZlUkpTRNJjUNQNIM1msR3mR5xDAmRSNVhgNHyYkBQwQpgmAJJKYTRG/vBMcPLmmVqoJVip6dSIxpii7L6rGhHtMEsiq1z1vidnkpigF5eXbF4gv59S9xgf2yt996e+3atQAAn8+3fMUZo6OjAIFoNGr3XQoWO1eoCLFwXXft2vXQQw8dOnRo/fr19913n9/vB0Wa4/zzzz/rrLNuv/32+++/P5lMXnPNNWV66aUrkG+UIGQCAABEuqEigIbGjx8eOMi6KJIzODfM6qOKphkEpFhDzBCs2/RXmSQJVBUpqkkxgKAQRUGXx2RZs3WRuft1ou9doJgmLQOKIHg/MAjYM9o9log3RBo5jgImreumjhSkEcn0ZEZKu13eglXNwWzeHVN2ZHh4YfuCH9/3E4TQVVdd9esHHwSwgOn5v8l4RwhpmvbII488//zziqLcfffdy5cvB8UbC5/nOO7uu+/+4Q9/+NBDDzEMc/XVV5cexCiBYtYVAMAwDEnOpDOJvsGjPn8wnh4dTh0nNY3RdVPS3H7DFxVHJ0yaNau8oKkBuXhIkyQkdIQgABSAoL8fjSR0lxeoGmxfofYdYymXyblNv49gWZAYJ1NytiYWrqkJ6yKVlCZIFmq6TNMcIgzD0ExkFnTl7G9aLFGidGpGTrFXXHkFy3ErVqwYHR398pe+VFUVGR8ff/nllzds2EDT9PtgZs0xsbD6u//++996660lS5Z85zvf4TiuHH7ga26++eZsNvvggw9WV1eff/75s6xMwXSXRHpyeGxgPDk2KU0MTh47PNHpD1LVPi9fFVcR0HTU2KYhQIQ8nMDSAWJFNXuOj1zIgiBBUAiYwI803YQEQqaZTCVXXHI0kxRlLTtqdPcbb4UbkqZqmpTMIDfpFpGqkLrg8Xur3PXN1Us4ijcMjSRJEtL59cw5Lh0usZ8sQDgA/rxt27FjvV1dXQAh00A1NTWhYFDVtJwrZ9O8pTGXxEIIxePxe++9t6Oj49Of/vTVV189LUscN9Btt912ww033Hfffa2trfX19TMQWvkCwIosS3IGIQNAkMhMZLRJycyyAmSDIhVSJF2vqoJuF+FiGYZiY/yiNvjXAqxDBgAGBACYAEEIEEIUAKIovvvuu6lUyjQpDnoYSnAT4Qa4dsR4YyT8UkY6miR6gcwdHNzr89HSILeyZX19tNXH+g1TV1WVZQiCoOxvVv63l/9nvu2FEIpURSLhqrGRUbfH89d/8/+2PL9FVVWKpudq2uApMWczoRFCyWTynnvu2b1794033viZz3xmZv4dTdN33HFHMpn893//93xLs/zK5J8xDF1WRGSaNE1LekYyRMjotQ0s71ezshKJwqCXCXk8PsFfz5zbpvwDkANZMZvNZrLZdDabzojZdCaTyWZHRkcPHDiQTqdBnrCJaGfWDHzK5wHD5Kud0hbDPXSoZzhNHese3j0yOYAAQBAeGzocT44iZICpkcQSYilHm5fbGhCMjo0++eSTVdFIJp2+47vfGx4aUlUVj7Sf8kFzgrnhLw6Z/OhHPzp48OA3vvGNSy65ZDbmZywWu/7663/xi19s3759w4YNs6+bZbkbhp6RM4Nj/X1jx5LmEBPKuPxZiqUaaxm/ILAURxGM11gSSn4uC2X7KmeGYWQyGcMwTNNMJlPayTrFBhgg5xkDm/RFz7kCOqKBIsPBo4x73vixsUNVnmqPy48AYmi2RAD9lH1c2vpECAEIV69aNTgwWFVVhRAyEQqHw4lEYsW85kwmg72oovfOEeZMMP7yl7/cv3//VVdddemll84yWAAh/MQnPvHEE0/8+te/Puecc6brIRYcAsPjzQzLJ4cTPUNdaSOusgmST5MMqImwHtbDki6K4HizWhj8aBaJBEEQBBGPxwcGBhRFMQyD53mPxyMIgvXRg5NYi6w/A8bi0f4eb9MRmjII0qAYfWQwsR+9kxIzDb72hmgLTTGmaeD0h2m9FCiv78dGR5944gmPx3P48GGP1/u3N9/0xGOP67rRsbvjogsvKlh4+TUpE3OgChFCb7755iuvvDJ//vzPfvazsy8QAEDT9DXXXHPkyJHXX399uvcW7ACEDM1QVE32CD7exatERqFTJI1CftbH+1yMnyV9HBHsfKb24IF3E4lEMplMp9MQQlmW+/v733333dHRUY7jdF0HeXN28kWOe3gja3o8Lt4rMJEYPHPV/MF0z96+N48MdxIkIamiYRpldmexUGoJxCcm3u3qFrPZpUuXplMpPEeyqip85ZVX+vy+0g+aK8x2URCEUCaT+ad/+idZln/4wx/yPD8nsU0IYXNz8xNPPDE6OjotEajrOk7szLFtDUNLZVKSJiKITELrntjJCkZtLRPx+jxcwEV7ecr97M/7vv/th55++qmnn376wIED/f39oijW1NTU1dVhqRkOh+0+Jnhv8BFZQhGDhHQ2pfjq0hAChAyaoSRDyqRMgfFEPHUcy2uawtIsSVLlvxossgZpvjALh8M8z2/60Ie6u7vfeP31VCpdXV3d0NCQSqVqamrss16tt8AlkCTJMEyZ9SmNOVCFDz/88NDQ0E033eT15kb/ZgOKoj784Q8//PDDfX19DQ0N5XeA9YlbrWYYmmGYCJgMxQxPHn+j6xWPy+0KqT7e5WaCAu1zkYI4AR780Vs4gppOpw/sPzA0OPjSSy8lk0mv1+vz+b7//e/byVTwoXbaUWOLgDhMQDPoYZLZEd6joRCryEbvaJfX5RudGFm5cB1Ns9PNaMhHvt7fvXv3rTffoqrqfz/6CElRuq4hhEiKOu+884yplSZAHqvmFrNShQihoaGhp59+uq2t7aKLLprb+kEIL7/8cl3XX3jhhZnVzTpACIlKRlblyXScYbjmWEskEPYKHi8bEOiAmwyyMPTYfxzMZhTrdpZlXC5OcPEkSSaTSbwKDV4jySrTLqKsJ5pToEhG6a2rD9X6+ICHFQSO492k189Jaqar70BWFBVVArbbgU3+5TdF/tsVcxIhhK2trX97881PPvN0e3v72MjIho0bDx7YH4vGtm/fbrkd1r0VSiWdrcT67W9/m06nb7jhhkrULxaLrVix4sUXX7zuuuuKLVuQjxyjhCAI04SyJh8b7DGQaUC5MdosJY7xrio/F3TTbob0aWn4+IM7LePJ6/WuW3fWmWee6fV4NE0bHh0544wzcDqG/SmYWJhG2GHE/vyJ/Hldzxz2tZyJdIoUWMLNZjQGkCRIZicJk6WAJErZI9nulvr20ll+GEUMx8Lml9vt7u7q+sGdd/7q1w/SDBONRuc1twwNDS1btjSVTIXCoWISdw4xK2KNjIxs2bJl9erVixcvnqsK2QEhvPDCC3EUY+nSpdO6ER+gE2tp0AzF1kYb3j1+sHtwT6w6sCi2LuQPyPAYSxE0wf/uwVfFrAIA8Hg8S5YsWbNmTWtzSzQaJQjizbfeFATBSne2a1i73Mrhlq7rmqbpEp/oT7tjPp2m3CypsPJkfFhJsaNSwlDR+GScAPCaK7/k9wRP+VkWVFglLK3bv/uPX/rKl8fGxgLB4GuvvXbo4MFAwJ9Mpdrmz88vsBJe4cyJhRB68skn0+n0NddcM4cVysG55557zz33bN++fcmSJWUKxcKXmWAsPnZsqGcyPREI8zX+NpNMGoBjCYEB7qf+580lSxavW7euvr5eEARBEIKBAAGJ/fv3cRwbjcVEUcyPeuRY7plMhmEYzDBN1zVV1XVt5DBVVes2SEZhKJnJKhTSaaCKGgTk8dH+mmD1RGLM7wkWlD3T7W/79ePj469tf62qKoxM82Mf//jY6CgCoKWlhSTJaRlwM8bMbSxFUZ599tn58+cvX758DvUgQsgwDKxQTNP0+XwrV6589dVXp1VCvv3h8wRa6lsbY/NC/pCLc8l6EgLkpmo4sqr3UJKl3ZdffvnChQurq6tJknzhhRf27dvHsMwFF120eMnSI0eOjI+Pp9PpbDYriqI0BVmWZVkeGxvbt29fNpvdu3dvOp2WZVmSJFnG/6ShwxpPB3jKJzBeL+/3u70C5xJ4l0dwL2tbtnbZ2VWBKsxMw9DN4jGIHDss59f8RKP/+tWvnnj88TNWrEhnMrfefMuhA4d8Xu/OnTutIBy0zaPMecScYOYSa9u2bePj49ddd91sHm9z3AysQbBCsVoKQtje3t7R0bFnz5758+dDCFmWLTNhy95SNM2EfJENZ1zk7uEz+oiBVAg5CtI05HZsP7BgQbvP53O5XHv27Ono6Pj617/e3Nzs9/shhA/95qHfPPSbUCjU2tra2toaCoVYliVJEldgeHg4lUoFAoE9e/aIoijLMgDAMAy8mpKqqiNjCY5aaVCEwZAay4p0EpCyW3A3hRdE/XWN1fNIgjQMHUICoamsniJfe0HDCBZaGQAh9KEPXTYwMDAwMODxeK67/rqnn3wqkUxu2LCBoqh8iVUJ+3iGxEIIbdmyhSTJSy65ZGa3AwCwIaKqqmHkfqn2V5VlGSGE1+OLRCK6rguC4HK5KIqyLPqCw212mwMhRBAkz7pXta6fyA4l1COqmaWBBwDYeaD32LH+tWvX7ty5s6en5+abb/Z4PAzDsCwry/LOnW9rmoY76bXXXguHw42NjU1NTT6fL5FIkCRZVVUFAAgEAtlsdnh4mOd5XdcxqxRFGRsb19IUzbg9jAfycpYzRA7GgvOaqlpJyPAsRxIUQghCACECgMydSVOyAcHJbHvvxQH44x+fPfPMdbW1tRCCPzzxxLtd3cg0X9u+/exzzhEEARSRT3MotGZIrJGRkY6OjvPPP3+6EVGEkKZp6XTaWirdLn4sKWWapqZp+/bt27lz5/79+8Ph8MMPP/zII4+wLMtxHM/zLpfL5/P5fL6Ghoa2tra6ujq/329t51fk0aZpGrqhMzTHIb9ijgIICEiKWXHv3r19fX0cx91222040kMQBE3TR3t6AHyvQF3Xh4eHh4eHDx8+vGTJkqGhIbfbfezYMUxx0zTHx8dJksTaMJvNZrPZTCZDE18HpKLpKQNIAuvmq6JAoccTQwLjp6mWKTGDLErZzXB7y5TjPJ4oAYClS5d+6tOflmU5kUguWLAwk84kk8n58+fjcIO9nL+gcANCaPv27dls9rLLLpvWXbhjVFVlWRacPLZvNVxfX9/zzz+/Y8eOeDwei8UaGxuXLVuGVY8d2JQmCKKnp6ejo0PTNEEQGhsb161bh70566FWwxmGoRuaqGSyWloyMyZlmKYBkEkSBAAgkUhACG+99dYNGzZ85CMfYVk2EokoskJTBcIcsizv2LEjGo0qimKaJsMwDMNkMpk9e/YQBGEPQgIAGVIAJCVDlaUMnkEpUdZ0fTKTqgnQmq7QJE2RJEIAwlxTqaDTZz+Tc41lNr322mv/8dOf3fX9H9z1z/cAhBYuXDg4MFBXV+/xegOBQIlYwxySbIYSa9u2bTzPr1mzpszrEUIjIyOTk5M8z5Mkqes6tG2cByE0DOP5559/8sknZVlesGDBmjVr7ASyN6Kl2gAApmkKgoAnkEEIJUn6n//5H5fLtXLlyhUrVuTkHhEEYeomx7gS8uhw6qjby7hYXkcqRZNWJbPZ7PPPP79nz57rr7v22muvoyiqYPJPNpslSdLr9eJYQyqVkmXZ6/XmsQoACDiG0yCiCBYCGRJAQ8pkJuEiggQBJVXmGQFCzKpTOFKYNHBqKlgJEgSDwc9fe+2uXTtXr16N6xOfjJMU2draai8tJzZWLOI6M8yEWBMTE/v379+4cWM5eQcIIVmWjx07BiGkKErTNEwXqw8URXnxxRf/8Ic/RKPR1atXw+Jb5hWLB1ptRBBETU0NQRCdnZ2HDh1auXLlwoULcWkQQoQABIAiGEM3Etk4yblkzcVSDCRzWzOdTu/atfPss9drmorMwm1tGMaePXtCoZDX681kMgghiqJyVkECAFAkSTG0big0SZGQME3TRAYgEElSLMMZhjalBMuNpBSzjXLE29GjR994/Y1DBw8xDBOPx0mCxHrm7LPPtj5mUEmdOG1iIYR27twpy/IpM6VwpScmJgYGBizhASHEoRTDMGRZ3r1791NPPeV2u88666ycqCbKy34pFg/MF2k4H/rAgQM9PT3r168PBAKmiZe2Il7bt2U4fcRgREZQBJ7hDAqR76XBWIVrmtZ79ChFUfYkmRzwPM/zvGmabrdblmVrRww7SJIE0ERIR8AAhElQCBI6JBDLMD5PUGDdJEkRBJmvB2eDjo6O+ob6W269ZWhoSBTFltaWXTt3Hu/vv+TSSycnJ4PBICiuW+cKM5FYO3bsMAxj3bp1Ja7Btezp6RkbG8NhQzDFKnzc3d39wgsvpNPp1tZWvGoZyIuYFxNR9mtAoTY68W4UhTXs4sWLFy5coBuKbugr2tb1DPt29m+hJdMt0BxDIiI3a880DVVVhoYGSZJSNa2Y0PJ6vS6XK5vNYulrmibHcTjiYIFlaROpBlANpJpAMZEGCERRJAKGokoG0g1DIwkqJ1O5fBR0DzVNy2azdbV1qqbGqmMP/Ow/auvrKIrq6uqyD2BUKM71wesAACAASURBVDSKMW1iaZq2a9euJUuWCIJQossBAL/5zW+++c1vyrJMUZTP51u/fv3XvvY1r9fb399/6NChvXv3BoPB6upqe9TKLquKFVtCbuVfhhDieb6rq2tg4Pj8xc0sx4lyxiuESEBn5cmUDFwcSdK5+ss0TUWR4xMTBEWqqmqi3AswxsfHZVlmGEYURZ7nU6lUfkY5w9IGkHVT1JGom5JuKqouRX1tPcd6MymJozkIyQDFnVJcFTQAcv60rqmtrf27W27dfOUVn//85zPpzGc+99ltf/5zdU1NOBy2HGfLVM3XiXOCaROru7s7Ho9v3ry52AUIIdM077jjju9973tYOKmqKori7373u8cee2zNmjXz5s2jKCoYDOaYupadnsOtgnEp64xFoxKX0TStqtpbr72zeHk772GHRo6PTcZ1ZYARAi6eQGS+xDJlWYknJhmaUjWt2GLchmEYhgEh5Hle0zTskeRcw/F0Wp3QkaihtGKkZU1UNKV7bK8oAYHyJ1KJoCeCpgYLQEkC2d/RMuRBoZDN+eef/8tf/WcoFIpEIqlU6tVX/9zd2Tlw/LgkSU1NTcXKnFtMm1h79+6VJKmYHsTG04033vizn/0sv8atra0tLS0cxxEEoet6TsYZ/tM6me8DFzQLCmrDgjLP6/Ee2ttdVRfsGjkwMjZOBZRENuV2MQStAwCBjT6GYUqynEolWJZVFbXoFoYAAABomjZNM5lMgkK0oGgCkaKqJmQ9LWopxVAUXdR0oi7c0hxZUh2s4TneKv6U8YX876fwQykKz+VUVVUQhGg06nELqqbhpRxyxGoOX0u86bQwvbFChNDu3bsZhlm0aFHBb8swjG984xs//elP86u4du3alStXQgjxx50Dw4YcK9juBqMpWH/mXGb9mn+LYRiC4J4cTo+PJHQVZDNGKpNKSHGTOCkfBgBgmkiS5FQqmUylFEUt0SCKooyNjY2NjdE07XK53G53boyDhLqZ1IyMpKcVQ0wpcdWQ6kPNbTVLvEKAYTgCEGhqFmuOw59/nO915sAq4eGHH8Zn/IHAvr17dF2vqalZvnx5TuPkNFTpwqeFaRAL+0r79+9fsWJFMRvo9ttvf+CBB3LOQwgvvPDCmpoaTdM0TbMPC+Yc2IcL89PoCrItv6Xyr7Gf4ThuVdPZDHIbCpFNo5ScNMn8NHYkS0o6k86kM4pagFhY/eHMUr/fj1NnRVHMZrM5XqQJzePjw5PZpGpIiq6IahoZFE/7JEXUdY0kCJzQhVDut5RzYD234MvmfG8AgMl4/OcP/PzIkSPDg4NfueGrR3uOIoT27NlD0zQ4WQnkv92cYHqqsKurK5lMrly5Mv8nhNAvfvGLu+++O6euBEFceumlpmmqqoojPZbWQwjhMVHrwCrKusZueIEiMeJ8HZFzGTrZnIeIuHjBR7cPPKVkj0tZzYRmjioEAEiynE5JKmuqhYjl8/kghIqiiKJoGAZN03gzJoqicvIBKRqmpCSAhoZkSTVMjZFl1Ccdhfpwc0SvDtYSkLReuoQdnU+FEtcjhOLxeHdX97qz1umGcdeddyKE8JisXRVWjlVgWsSCEB46dEiSpNWrV+f8hBB65ZVXvvWtbzEMg2ccSJJkGIbb7V67du3AwAAOkXMcR9O09XlhMuH3tP40TRMHuix6Wf/ba1KsUQoa/jkdYJomCcmz6jZtG35Uk7KITOWXoyhKNitmMtmCUwgVRcG7fEUikXg8jgP0JEni/aHsdaMYw+NTREVOZaS0qGayQMzoI4lMmK2DEUI3sXh7L2JZJkrIaVzUZZdf3t9/HItSBJDb6/V6vNHYicGuilIKYxrEQgjt37+fIIj8zWf7+/u/9rWvLViwoK6uDgcnMWKx2B//+Mf+/n6apoPBYDAY9Hq9HMcxDIOlVw6Z7JRCU8mZOMpFkiTuPJAXXCgWgACFlkLAFxiGwRLCGcEL+pQduj6WpwqBrutZMQsAcLvd2PXDowVgSg/ikyzLCoKAg3MQQoZhcuJYDEem5ZQoq2klNZ6WUgkqkyCNNF/jYglE6JpmmDoFaNzROXXOafzymYcQev311xctXrRv377q6urlK1aMjY70H+9Pp9N45L5Yc80hpkEsPIixZMkSrKctaJp25513Llq0yOVysSxLTCEYDD733HPd3d0AADzaPz4+HggEQqGQ3+/neZ5hGGRL7UV5yGGb9acVUAV5IgqczKF8nWhdr+t6lG+JT45oYoFlFBFCoij6fL7GxkZ8PX4p/MEoikJRFFYuHo+HIAgrKdnv96uqKsuypmmKoqgmTGRFRVPjqawkEpm0logrjEpDk6ipqjN0/PIA21j5Y1kl+rsECwEAPp8vmUySJIkAWLV61fY/b9M0bXxiPH9X6QpJr2nsf3f8+PGRkZGcRWAQQo899pimaeFwmKZpq/V5nj948GBHR4d1JZ6ljunl9/vD4XAgEHC73TgujxWimQeLVfY/8f92EZLfOgUjrjmdoWlKu3/tbnKfxzOgqqqmaXa3yzAMlmVDoRCYSr6wiJXJZDwejyzLOLvGssOSyaTH47HcEU3T6CB69U9ipF7REUiOwGwCIIkROJ/XFRAl0ScE8DYqCCHTNCA8SSfmvFSOdVU6Op1MJn/5819sefGFRCLxyksvdbzT8aWvfLm9vd3lclmNkNNuc8uwaUiszs5OSZJwgMSqyuDg4JtvvomjnVaNSZIURfHJJ5/MLwSbX7Isx+Nxr9dbVVUVCoU8Hg/HcYZhYP1oSa8SDMNqEUsve0QH5UW2kM20zzO2AIngGXXre5sncF4ezjlWFAXHRGiaxgMM0JaoAyH0er0QQstgx64uRVEURQmCgL3aE7ETryxlMxMjFIU4kOaCNCtSKkuwhmbEExN+IQABIARcLEEQ7wmt6Xaz/fpsNrugvf3c884bGRnx+XzJZMolCIqijI6OBoMnTdyonLE1DWIdPHhQkqQcl/DJJ5/0eDwURUEILUJQFHX//feXFuM4ApRIJIaGhqqqqsLhsM/n43mepmnL/LJUjAVJklwuF+aZaZrpdPrdd98FAHi93iVLlgAA7FqyBCyG6bre3NQSi8VEUcQyBkNVVbyus8vlsohlyWNZlvFC+zzPK4rC8zyWoF6v16I+QsgwDMmVqmoVKY4EmWCsuo0FwujYhI8PLm5dfqBzbzI5WVNV21Tfwvh5nDYzs57OuevZZ5/9t3v/ddXKlSzLQgguv2Lzbx78dTwez4k+VtSEnwax3nnnnXnz5tmnO3d2dg4PD3MchyUHAAAT4oEHHkilCrha+dA0bXJyMpVKDQ8P4yEIv98vCALWj5g9+OvPZrOJRALvshyJRLDmlWU5m80CAPCgCq6GBXDymI/1UMv2wr+SJLlo0aLu7u6cWJqmaQRBMAxj8cmegWOaJs/zBEHkj5lacTfTRJDVISKQKET4usZQayTQ+C5xoDHWGvZHyQVEb39PIpUAAJEUZeXRnxIFfV5gE9LLli696OKLOna9Mzg4KInSk088EYlUmaa5a9euiy++OMdEPrnm728+FkIok8l0dnba1ydCCL322mtW7jmcyoh64403sBQpH4ZhpFKpTCYzMjISCoWwfnS73dgVkCQJLyHEsmwwGMxkMseOHRsYGAiFQlgZAQBwQN9SkRa3LLsb/2RdbG9B0zTb2tr6+/sRQlipWQFbhNDUwor4rSFufJIkca6LFZs8ubMJCAhIIAiBQasTR/m1K9ojQnNNcJ6Ldi9sPsPr8lZX1UVD1TWR+mRyApnI0DXA8Kd0/Qp2fI6DAgAYGRlBCG3YuHH58uWyLH/2c5/7h7//zre/8x2cPlm6kLlCuRLr0KFDoiguW7bMOjMxMTE4OOh2uy1DB7vxBU2rcmCaJp5cNTY2FggEmpubBUHAdOF5PhQKYcc+GAyqqppIJEZHR+02rEUswzByRFcOyXI8I9M0g8Egto1omsaswv+j91IAADghigAAiCDg1J8nOIcQwNfYWAhkIzOSHFpStbDRu7KhuplnXWJWDPurgt6Qi/cKLuj3hRPeMZKkdEOfwcJGxYi48dxzxycmvvPtv//Q5ZdBCEmKam5psd6lYFHlP7dMlEusgwcPptNpu4G1bds2TdOwxWpZIc8884wkSbOpEMMwtbW11dXVBEFMTExks9lwOIwDE9jJxzZ7VVVVNBpNJBKJRELX9Xg8jsOVeIS7GLfsUQN7+EBVVY/HI0kSzuzD9MJMPdmvRAAQuCMgxP4jPgkBgAhBAAAECEEzIY2lyWHKq9dyVRF/xMeHXKzbxbo9rM/tClAUDSGUFZGhOb+vCiFkmjpCpWKk+RzKv9jOj7GR0Vde3fr2229LovSDO+4MBAMAAGw1lrhrDlEusXbs2OHz+Zqbm60z3d3d99577xe+8IWlS5da3fOnP/1pxlXx+Xy1tbU4vzEejw8ODiaTSYIghoaGwuFwJBLBcoVhGJIksWnv9XoDgYAkSYlEYmBgYHh42Ofz1dTU4Kgmpo6dYTk2uEUscGKzLhK7tFgbEgRhn0o01QEGhAR4b9c0nMAIcEdLxqTKjyF3ihDkEPC6XL4ab/2a+WfWhhtpinFzXppmp3LbEU2x2I4EAODF38tBwfBVAXJA0NfXJwgCy3PXffH6N15/HQAwNjbW0NBQuuS5wqmJhV2nHTt2rFq1yj50j8dc77///k2bNn34wx+mKGrr1q0lEnlLIxAILF68WNf1gYGBoaEhK35tGEYymcTWfTgcrq6ujkQigiBYASeaphmGicViWIBNTk4GAgHL4sYsKSa6oG1CB0IGMg2KojRNzw9knBytMC0NiH83kJ4lRjRh1BUVOZfOcTQBKBfJE4Y7GqyeV9vuF0KmYUCCtEpACCJkkmTu9NFiKC3M7Mf4Sl03vnbDV1986U+RSOT3v38sEg7bBxfsIbFynj4DlCWxcIbx1VdfbT95YuNahJ577rmDBw/eeuut05oIn4NkMnnkyJHR0VHDMKLRaGNjI1aytA3YTspkMnhhWctHYxgmEokwDOP3+4PBIB6zszhUUCfmcAshFI1G/T5vx+4OACEEJEInfFK7aWw7BgAgCCHH8u4AonxasKbdJJoRk1XMdMhVBwGTSmdESWmtXqZpMkImRbEAAIQwq3QIKYIg7R1cjkt4ynimVcilmy71+X3vvvvuyMjIP9x++30//nH+sBXI82PmEGURa/fu3bIs50z2slfo2LFj//3f/z02NjbjepimOTQ0hI/xsE9O2BNP78HmOdZf8XicYRi3241DYuFw2BpPtA6K0QurPMs0NE2T47lFS9oBqbyx8w0xbYR8UWyonRy1hxACCAkTaNGqcFNTU1NTs9vLZo3JumgLzwo845b0NEUwCKH9vW8F3bHqUCNH8wRJgak8UdM0cTn2kIfVDmVGHOwRk/yfDh448PGPfXz+/Pn/cu+PVEW5/bbbAACoUB6R/a5p9dcpURaxtm/fTtP0qlWr7CdzJtDt2LFjruqUSqXwMBw2n9va2vDj3n77bcMwlixZEggEAADd3d0kSS5cuFAQBBzQwsHVfGIVE1qWxDJN0yXwvqCnfl69BqXDh48e6jxUX9UM4UmzPFRDmVCPZ8jBS9ZsWn/GxdWRRpanEVAzqmAAzcV5CEh5qBOrT61o2ahoIktxEJIQQDDlM9qHB+wfDx7GBqeyovLvzcfBQ4fuuPPOcFUYD3SSBGGW5E0Jh3HGKMvG2rZt26pVq/D0ZQs55lTOqP5sMDg4+F79KKqtrQ1/atj/j8fjeEFpRVHwKgkulyuZTOKJTYIgsCybT6/STiJeFLkqWEuSdCxcO6+2fUFT//4Dh44PD4Z9MQAAIA3WTVC83DfRe9bi9W2tbaGIPxD0Awh0nRpIHKYILuSuJmxTFUiCpknWPg3VIkQ+dSzC5ZzP72yYN+huJyWYmv07b948WZZFUbQuIwjC2v2rIMoUlmXiFMRCCPX29g4MDHzqU5/KeXDBDLhKIOd7wsMmw8PDuq57vV6v16vr+sGDB03T9Pl81uAjy7I0TecoRzuwKsT0omla13WKYkO+mOk1vJ5gOBitikZ37HnNVGBD7bzauhqP100yhGR+xO8NhXwxvysEACAgyVD8yPiQoooBIRr0xCypY5o6ASkIC8zpLai/chyF0q1RrED8KwHhv/zzD3d3dPz7z3668bxzz7/ggo0bNzY1NZWOULzfEuuNN95ACOE9yuwof+3GGQBvvkpRlDUnESHEsmx9fT1ON0AINTc319TUIIQoimpoaOjq6hodHY3H4263OxwO4znKOLWQnEIx0XUivgAJSNGmSQouyNay4apIe/sCQzc4zm0iFRKIIKGbD9AUD0/MLyUAAAiAFW3nHhs5RJE0mEoEhRCSJA0K+V9zbi/nPyIYCs2fP/+LX/ri+vXrL7roooKarnL+IMapieX1es8777z8rNGKguO46upqYJP8CCFMFNwi1dXVBw8e1DStqanJMIxYLDY8PJxMJnVdTyQSqVRqaGgIpxbax7btosvOMNuMZ0gQJEGQNM0ytOFxBwBCAALdUBEyAUA09V4a43tN5AosbTpHN1Xd0PAW9jkoXzCU6R4CmymWz9QLL7zwwgsvzOdTTuF2c+0DMN43b95ccBbh3KrkHGiaNj4+jtf1q62txTbW6Ojo6OhofX19Y2OjaZrhcLizsxMA0NTUhBCqra3Fc7AAAKZp4tX3RkdHPR6P3+/Hm0pg/YgZZje/sCo8OVgFSBLnTJsAQIpkT/RAIYWFe5AkaIRMu6lUkCXFvDmMYueLEa6Ee1is8BIcmkN6nYJYJarrcrlCoZAkSdjYmnFotCAymczhw4cBACRJWsTCPx0/fry2thYH3/Gf0WiU47hAIADzMtcURVEUBQcmOI4TBMHtduM1FywjDCtcVVXxGjJ4YXc8/g0AwI4hjjKc8svGyhHadk8taKeX3w7FRF2JCEX5JlppL2GWmPlSkTh9BSfEIYQGBgbmllsW0BTwn3hPcoQQHpRECI2NjdXX1xMEwfO83Q+yYJomXi80kUhgEYUX7OOnEAgENE2bmJiQZbm1tXVycnJychIAQJIkngOi67qVSQHyuqeY6V1Cr5WIQpVuCvs1M7u92Mn323gvBpxdiRMBEEIMw1SUWBDC+fPn0zTNsie2zhodHcUXpFIpfA3HcQWJZQdO8MLLZoCpfFe327148eJQKJTNZiVJwuUrioJJLEkSzudJJpMcx1EU5fV6raWzrM4upqrwQelfwakMHXRSnPYUYi+/MqUpVQnMnFgUReFP2T5VsBKwxJW1q7Rpmn19fXhgBwAgSdL4+DievDCDwrG9H4/Hx8fHw+GwYRg4RQIbXizLSpIkCIIoioFAAD8Cz5UIhULDw8MQQrxaqdvtdrvdVvfDIvnQJWpiHZSvLtFJA+SFby9TiM4tZk4srFBw2BpUMvqQTqdxurC1GG4mk7FLR1mWu7q68DEOSs0gxoa9gWw2m06n8eghFsOapmGnMpVKYf8xHA5jRQkAUFU1Go3i+TCqqk5OTiKEZFnmOA4n05bYHLAYCnZ8aa6UKK3Yrzk8nnPHcObEwt80JhYqNHVpTmAYxqFD703PwrxxuVx42U88OM1MASfdjoyMYMN/WoAQ3nXXXVdccQX2Gy6++OJ58+ZJkjQ4OGh1Kt5Fcnh4mCAIPDcBkzidTnu9XjzVO5VKtbS04JPBYLDS4aL8t7COCz46X3yWKVCni1kRy5oFP4fEIgiC4zhsymB/DQcI8AFev1RRFDxbS9d1S57hpVRylwCdDkzT9Pv9hw8fzmaz27dvP3r0KF67dmxs7KqrrsLJgDhDH7+yLMv406qvr8eZhqFQaGJiArsI2Fewzw6dPcrs/hJa9X2z32dFLPvknFlq7ra2NpfLhUWOoiiyLOOsc1mW8drdFnVy6mAlZkWjUa/Xi2f+zKwOF198cVtb286dO2OxGEEQBw4c4Hkem+odHR041fG66647fvx4X1/f2rVrY7GYqqoDAwNWf6RSqVgsZoXHLLdjFg2Ti9LGuz2EBsoLKFSikmA2xML+FCgeCZwWBgYGrGRz+3liavIPAIBhmMbGxrGxMVEUPR5Pe3s7RVHpdPrAgQO1tbU+ny+bzba2tnZ0dMxAbiGEzj33XADAZZdd9tJLL0UiEZ/PNzg4iAeXTNNctWrVxMSEqqoHDx4MhUIjIyO7du3CW56sWLFiYmKisbFx8eLFk5OTY2Njln6ZQ4lVvvk/3TLtA9gzqFhBzIpYxNTsF/vo+ozh8Xhomk4kEjRNezyegYGBRYsW4TBmZ2dnKpXC6w5omiaKIoQwnU4fPXoUb94UiUTwfFqfzycIQpmTz3KAW9nr9X70ox/FZ3D+qsvlGh0dxZti0jTd0tLS1dW1bNmyRx999B//8R//9V//Fef2bNmypaOjI5vNbtq0qbOzc9OmTdjGr5wjVix+VvC9MKxUiEobf7MyjODJKeSzKSoUCuHkT4QQThzF5Xd1dWGTBQDg8XjGx8ctGeByuRYuXDgxMQEAIEnSWhZmNuG0nOYmSTIQCODB72uvvfZb3/oWntIjiqLb7V6zZo1hGKtXr37nnXcEQVi8eDGEEE+G/q//+q/u7u57f3RvycUA5wDIhmndNa3zM8CsJJY1Fjv7LxLbwnjxI2CLOuIsP/xp4ulfeLEaAACep489r2w2izOQ8Gzpmb1OQdGScwZCiHdrAgB85CMfwbe0t7czDOPxeBBCq1evDgQC7e3tjz766MJFCxE4aaHJWTZUiShDaasr58qckzkB/TnBrDbCtMc/Zs8tl8tFEESOFuvu7g6Hw1aIf2xsjCAI7NsbhjE+Po6N5aNHj8ZiMQBAV1fXbBqodKA8/2KE0HnnnYfpFQqFFi9evG/fvsHBwcsvv3zfvn12Tzk/kjndLIaC9SxxS0HfsMQ4Ad6TsZwqlYM52GwczJHRNzExkZM1jxBqamoCAPT29vI8jz0+PIIkSdKxY8fwykoAAMMwent7Z/P0Ej1X4u1y0k44jsMzA+rr69esWVM6mF7wEaWVVH5Av0SZOYUXe5xpmhMTEyRJhsPhcDhc7E2ni1kRaw7jH3gIDx9LkoTtpL6+PryBBT6Jf7VGcgzDCIVCmqZhIYdTshobG48dOzaDCuA1RSVJwon29jUEC2qT/DN2v71gAAnacstyCjmlBCpxb5kl5J/EW3u6XK62tracvPPZY7YSawaWY0HY89zxWkIQQjxOjD+mkZERAIDX6w2FQpOTkzhYhbNc8F0QQr/fb55qXeFigBC6XC6cFY4n+xuGgcOwcGqKmHVxvhdsmZsYOaTMkRn5VxYTP9M1YUtHRNHUaoajo6MEQUQikebm5rmyknMwqz2h7WtZzbIeBEGEQqF4PI4XUMDB0vHxcQBAMBi0xubw7g9WCNTlcomimMlkAACyLPv9fkueTRf29iUIwu12Wz/hpbOsFY5wWDjn9hzSFOzdgirplHq2NMo34SGEeOUVQRDa29stEVWhUMisBqExpeaEW36/P5vNYnmjaZp9Mc9EIlFfX48tqpy70NTauAAAPANsxkM6drs1p61ZlsV+KIYoingYACtNvIxqMSOpdLcV8xXyCzyl8V5C8hmGMTIyAiGMRqMtLS0VElE5mANizYnESiaT1dXV8XgcJ1Thla7xT263WxRFS8dh+YRhN6dmuRhJ6ewMezcIgmC9r2EYeMU2S2niZaHxr/aWgXn5gAUVYs5BDkq0c0Hm4fR/l8u1YMECa/PREq85h5jVWKF9rb1Z1kMQBNw3+E9JkqwyLT2IUaFpZ6Xn3OXA6h48kmj1N14HEPMMr1qTzyerEDuHcrRkMYGUc6aYRsY72QIAqqur3zcRlYOZEwtPzMLrbYJZO4apVMoewSq4unpFYV+pcLqw+oxlWSvBFcdELOMMLxxXzLguYYTlPMu6Mkf44fPxeDyVSgmCsGDBApxL/T7zycLMiYXX3zVP3pjkfy9isdhc9YElIQRBsPQmXu3S8gAghHjRL/uNxUyu/MJzoGkadqtramrw/o8fFJ8szJxY2Le3+4ZzWK33HytWrKhQybiP8aaNVithIxKnA+FtSHI2fTklM/A14+PjyWRSEISFCxfiRdU+cEphzJxY8+fPRwjZN1Saw2q9z1izZs37MyPX6nWczAimDHxRFHEKmqU07dfnW2mapuE8sPr6+ra2tr8EEZWDmROrurp6+fLlb731lrXs3dzV6n1Fa2vrPffc80HVH1tLeCIGAAB/q5lMBpv/WGnajbN4PJ5IJHAyxV+UiMrBrPJyBgcHv/KVr+AZBJ2dnfF4fA5rViHgVH2PxxOLxVpaWpYtW3bOOeesXLkyFApVKG1/xrBcPEmScDREVVU8Wa10ZPUvAbOKvPM8v3nz5gcffHA2meYVAs4S43k+HA7X1dXNmzdv3rx5TU1NTU1N9fX14XAYLyQO/lK/eDBVMTxhBJ8RBOEDrdE0MKuxQlEUGxsbFyxYsHfv3rmq0HSBB/LwOpE1NTWNjY0WhxobG6urq/HGGX9p0ui0x6yIFQ6H6+vrL7root7e3ven57AiiMViDQ0N8+bNa25uxkKotrYWp3rmB9CLRbEdVBSzyiDFa7Kn0+nLLrvMmjI6J8CTwEKhUF1dXVNTExZCzc3NDQ0NVVVVOJW0YJWKVXUO6+agHMw2bcbr9dbV1S1btuziiy9+8MEHZ1IDivL5fLFYrKmpqaWlxVJk1dXVXq8Xr56Qf5fDlb9wzDY1GQAQiUSy2ezmzZt37dq1f//+YheTJOlyuaqqqurr67EQam5unjdvHl6kD294VOwRDv7XYQ7sDxzfO3LkyK5dux599NFDhw7hndYikQi2hDCNGhoaotGo2+2e2vbIVgmHPacd5sawxeFjPOPPMAy8ylnBhDiHQ/9H4HhMDioCJ7rjoCJwiOWgIpibeYUO/rdjZhZRCYvZIZYD7HuZP/vZz1RVxZvHnup6AAAgCPKGQuasZQAAA/ZJREFUG24otsO0Y7w7AAihF198IRgMrlr1XlIaTj8scdehQ4e6urqslXly4NhYDgCEMJFI1tXVAwAMw/j5z39+7bXXPvDAA8C29EsOAADV1dXJZNE17hxiOXgPpmneeOONHMfNn982y/xNx8Zy8B4efvjh1atXv/DClptuupnjuHQ67fF4ZlaUQywH7+HVV19dtGjhX//1t37yk59omjo5Ofm3f3sT3hjL7XYfO3YM7zafSCQGBwfnz59foihHFTo4AYTQqlWrXC4hHA5v3frK5z//hcnJ+LPPPvM3f/P/vv3tv3v77bdvv/22l19++Wtf++rhw+/effddExPjJUpzJJaDE4AQfvWrXwUAyLJcW1vX29tbXV2zZs3aWKwaLzn+mc98LhQKXXDBhVde+eF9+/bh3dGKluaEGxwghH7/+99Fo7FQKAgADIWCqVT60UcfbW9vv+KKKw4fPoxn4FqXAwAAgOl0urPz0Be+cG3BMh2J5QAAAEiSuvrqT2Ep43a7v/nNGy+77ENDQ8NXXfWJjo53ikgf+IMf/KBYgQ6xHJyAFaASRfHuu+8iCPLss88aHx/Hu9329h5bsGDBkSNHamtre3uPbtiwoXQyumO8OygMnue//vUb77rrbp/P/8lP/hUA4Otf/4bP5/urv/qrhQsXfeMbN7pc7hK3O8RyUBQuF/+f//lLkiSrqiJ44YmVK1fhXV527tx51VWfKGGfO8RyAAAAkUgk54yiKLfd9p2XX34pkZjcuvUVQXDdffcPAgH/L3/5Swhhb+/RX/3qV7FYtFiBjlfoAI/boEceeeTtt9+y08G2GMkJT9B+18qVK6+++jPFVkJ0iOXgBGbAhBL5WI4qdFAROOEGBwBMzeF744038JpKHMc1NjYODAxEo9HJyUlVVU3TXLBgwa5du1pbW91u944dO9asWcOybNHZ544qdAAAQAglk8nf/va3v/3tb9evX//mm29ef/31v/jFLz71qU+RJPn4449feumlbre7q6urs7Ozubm5tra2r6/vvvvuK0YsRxU6AAAACKHP56urq/P5fN/73vcikcgzzzwTjUZfeumlxYsXNzY2Pvvss3/605+uvfZaAEBPT89NN93U29vr2FgOTgGE0OHDh2+44YbNmzdv27Zt48aNvb29F1xwQV9fH8Mwra2tVVVVkUhky5YtmqZFIpHf//734XC41Lrzjip0AABACA0PD7/++usAgKqqqgULFvT29jY1NfX29q5cufK5557z+Xzr1q17/PHHV65cGQwGn3/++U2bNpXYLcwhlgMAii80Xxqllg13iOWgEnBsLAcVgUMsBxWBQywHFYFDLAcVgUMsBxXB/wfMoi2P3ap41gAAAABJRU5ErkJggg==" }, "Event": "nodeNaming", "TimeStamp": 1593370496, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGR223 Roller Shutter Controller 3", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Motor Control Class B", "NodeSpecific": 6, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0303", "NodeProductID": "0x1000", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Window Covering Endpoint Aware", "NodeDeviceType": 6400, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ], "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ], "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ]} +OpenZWave/1/node/37/instance/1/,{ "Instance": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/112/value/5629500165193748/,{ "Label": "Switch type", "Value": { "List": [ { "Value": 0, "Label": "Momentary switches" }, { "Value": 1, "Label": "Toggle switches" }, { "Value": 2, "Label": "Single, momentary switch" } ], "Selected": "Momentary switches", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 37, "Genre": "Config", "Help": "This parameter defines as what type the device should treat the switch connected to the S1 and S2 terminals. This parameter is not relevant in gate operating modes (parameter 151 set to 3 or 4). In this case switch always works as a momentary and has to be connected to S1 terminal.", "ValueIDKey": 5629500165193748, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370186} +OpenZWave/1/node/37/instance/1/commandclass/112/value/6755400072036372/,{ "Label": "Inputs orientation", "Value": { "List": [ { "Value": 0, "Label": "Default" }, { "Value": 1, "Label": "Reversed" } ], "Selected": "Default", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 37, "Genre": "Config", "Help": "This parameter allows reversing the operation of switches connected to S1 and S2 without changing the wiring. Default: S1 -> 1st channel, S2 -> 2nd channel. Reversed: S1 -> 2nd channel, S2 -> 1st channel.", "ValueIDKey": 6755400072036372, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} +OpenZWave/1/node/37/instance/1/commandclass/112/value/7036875048747028/,{ "Label": "Outputs orientation", "Value": { "List": [ { "Value": 0, "Label": "Default" }, { "Value": 1, "Label": "Reversed" } ], "Selected": "Reversed", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 25, "Node": 37, "Genre": "Config", "Help": "This parameter allows reversing the operation of Q1 and Q2 without changing the wiring (in case of invalid motor connection) to ensure proper operation. - Default: Q1 -> 1st channel, Q2 -> 2nd channel. - Reversed: Q1 -> 2nd channel, Q2 -> 1st channel.", "ValueIDKey": 7036875048747028, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} +OpenZWave/1/node/37/instance/1/commandclass/112/value/8444249932300307/,{ "Label": "Alarm configuration - 1st slot", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x00, 0x00, 0x00, 0x00]", "ValueIDKey": 8444249932300307, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} +OpenZWave/1/node/37/instance/1/commandclass/112/value/8725724909010963/,{ "Label": "Alarm configuration - 2st slot (water)", "Value": 100597760, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 31, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x05, 0xFF, 0x00, 0x00] (Water Alarm, any notification, no action)", "ValueIDKey": 8725724909010963, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} +OpenZWave/1/node/37/instance/1/commandclass/112/value/9007199885721619/,{ "Label": "Alarm configuration - 3st slot (smoke)", "Value": 33488896, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 32, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x01, 0xFF, 0x00, 0x00] (Smoke Alarm, any notification, no action)", "ValueIDKey": 9007199885721619, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} +OpenZWave/1/node/37/instance/1/commandclass/112/value/9288674862432275/,{ "Label": "Alarm configuration - 4st slot (CO)", "Value": 50266112, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 33, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x02, 0xFF, 0x00, 0x00] (CO Alarm, any notification, no action)", "ValueIDKey": 9288674862432275, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} +OpenZWave/1/node/37/instance/1/commandclass/112/value/9570149839142931/,{ "Label": "Alarm configuration - 5st slot (heat)", "Value": 83820544, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 34, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x04, 0xFF, 0x00, 0x00] (Heat Alarm, any notification, no action)", "ValueIDKey": 9570149839142931, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} +OpenZWave/1/node/37/instance/1/commandclass/112/value/11258999699406865/,{ "Label": "S1 switch - scenes sent", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 40, "Node": 37, "Genre": "Config", "Help": "This parameter determines which actions result in sending scene IDs assigned to them. Sum of: 1 - Key pressed 1 time. 2 - Key pressed 2 times. 4 - Key pressed 3 times. 8 - Key hold down and key released. Default setting: 0.", "ValueIDKey": 11258999699406865, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} +OpenZWave/1/node/37/instance/1/commandclass/112/value/11540474676117521/,{ "Label": "S2 switch - scenes sent", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 41, "Node": 37, "Genre": "Config", "Help": "This parameter determines which actions result in sending scene IDs assigned to them. Sum of: 1 - Key pressed 1 time. 2 - Key pressed 2 times. 4 - Key pressed 3 times. 8 - Key hold down and key released. Default setting: 0.", "ValueIDKey": 11540474676117521, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} +OpenZWave/1/node/37/instance/1/commandclass/112/value/16888499233619988/,{ "Label": "Measuring power consumed by the device itself", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 60, "Node": 37, "Genre": "Config", "Help": "This parameter determines whether the power metering should include the amount of active power consumed by the device itself.", "ValueIDKey": 16888499233619988, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} +OpenZWave/1/node/37/instance/1/commandclass/112/value/17169974210330646/,{ "Label": "Power reports - on change", "Value": 15, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 61, "Node": 37, "Genre": "Config", "Help": "This parameter determines the minimum change in consumed power that will result in sending new power report to the main controller. For loads under 50W, the parameter is not relevant and reports are sent every 5W change. Power reports are sent no often than every 30 seconds. 0: reports are disabled. 1-500 (1-500%): change in power. Default setting: 15.", "ValueIDKey": 17169974210330646, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} +OpenZWave/1/node/37/instance/1/commandclass/112/value/17451449187041302/,{ "Label": "Power reports - periodic", "Value": 3600, "Units": "second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32400, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 62, "Node": 37, "Genre": "Config", "Help": "This parameter determines in what time intervals the periodic power reports are sent to the main controller. Periodic reports do not depend on power change (parameter 61). 0: periodic reports are disabled 30-32400 (30-32400s): report interval. Default setting: 3600 (1h).", "ValueIDKey": 17451449187041302, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} +OpenZWave/1/node/37/instance/1/commandclass/112/value/18295874117173270/,{ "Label": "Energy reports - on change", "Value": 10, "Units": "0.01 kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 65, "Node": 37, "Genre": "Config", "Help": "This parameter determines the minimum change in consumed energy that will result in sending new energy report to the main controller. 0: reports are disabled. 1-500 (0.01 - 5 kWh): change in energy. Default setting: 10 (0.1 kWh).", "ValueIDKey": 18295874117173270, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} +OpenZWave/1/node/37/instance/1/commandclass/112/value/18577349093883926/,{ "Label": "Energy reports - periodic", "Value": 3600, "Units": "second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32400, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 66, "Node": 37, "Genre": "Config", "Help": "This parameter determines in what time intervals the periodic energy reports are sent to the main controller. Periodic reports do not depend on energy change (parameter 65). 0: periodic reports are disabled. 30-32400 (30-32400s): report interval. Default setting: 3600 (1h)", "ValueIDKey": 18577349093883926, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} +OpenZWave/1/node/37/instance/1/commandclass/112/value/42221247137579028/,{ "Label": "Force calibration", "Value": { "List": [ { "Value": 0, "Label": "Device is not calibrated" }, { "Value": 1, "Label": "Device is calibrated" }, { "Value": 2, "Label": "Force device calibration" } ], "Selected": "Device is calibrated", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 150, "Node": 37, "Genre": "Config", "Help": "By setting this parameter to 2 the device enters the calibration mode. The parameter relevant only if the device is set to work in positioning mode (parameter 151 set to 1, 2 or 4).", "ValueIDKey": 42221247137579028, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370129} +OpenZWave/1/node/37/instance/1/commandclass/112/value/42502722114289684/,{ "Label": "Operating mode", "Value": { "List": [ { "Value": 1, "Label": "Roller blind" }, { "Value": 2, "Label": "Venetian blind" }, { "Value": 3, "Label": "gate without positioning" }, { "Value": 4, "Label": "gate with positioning" }, { "Value": 5, "Label": "roller blind with built-in driver" }, { "Value": 6, "Label": "roller blind with built-in driver (impulse)" } ], "Selected": "Roller blind", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 151, "Node": 37, "Genre": "Config", "Help": "This parameter allows adjusting operation according to the connected device.", "ValueIDKey": 42502722114289684, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} +OpenZWave/1/node/37/instance/1/commandclass/112/value/42784197091000339/,{ "Label": "Venetian blind - time of full turn of the slats", "Value": 150, "Units": "0.1 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 152, "Node": 37, "Genre": "Config", "Help": "For Venetian blinds (parameter 151 set to 2) the parameter determines time of full turn cycle of the slats. For gates (parameter 151 set to 3 or 4) the parameter determines time after which open gate will start closing automatically (if set to 0, gate will not close). The parameter is irrelevant for other modes. 0-90000 (0 - 900s, every 0.01s) time of turn. Default setting: 150 (1.5s).", "ValueIDKey": 42784197091000339, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} +OpenZWave/1/node/37/instance/1/commandclass/112/value/43065672067710996/,{ "Label": "Set slats back to previous position", "Value": { "List": [ { "Value": 0, "Label": "Only in case of the main controller operation" }, { "Value": 1, "Label": "In case of the main controller operation, momentary switch operation, or when the limit switch is reached." }, { "Value": 2, "Label": "In case of the main controller operation, momentary switch operation, when the limit switch is reached or after receiving the Switch Multilevel Stop control frame" } ], "Selected": "In case of the main controller operation, momentary switch operation, or when the limit switch is reached.", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 153, "Node": 37, "Genre": "Config", "Help": "For Venetian blinds (parameter 151 set to 2) the parameter determines slats positioning in various situations. The parameter is irrelevant for other modes.", "ValueIDKey": 43065672067710996, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} +OpenZWave/1/node/37/instance/1/commandclass/112/value/43347147044421654/,{ "Label": "Delay motor stop after reaching end switch", "Value": 10, "Units": "0.1 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 600, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 154, "Node": 37, "Genre": "Config", "Help": "For blinds (parameter 151 set to 1, 2, 5 or 6) the parameter determines the time after which the motor will be stopped after end switch contacts are closed. For gates (parameter 151 set to 3 or 4) the parameter determines the time after which the gate will start closing automatically if S2 contacts are opened (if set to 0, gate will not close). 0-600 (0 - 60s). Default setting: 10 (1s).", "ValueIDKey": 43347147044421654, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} +OpenZWave/1/node/37/instance/1/commandclass/112/value/43628622021132310/,{ "Label": "Motor operation detection", "Value": 10, "Units": "watt", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 155, "Node": 37, "Genre": "Config", "Help": "Power threshold to be interpreted as reaching a limit switch. 0: reaching a limit switch will not be detected 1-255 (1-255W): report interval. Default setting: 10.", "ValueIDKey": 43628622021132310, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} +OpenZWave/1/node/37/instance/1/commandclass/112/value/43910096997842963/,{ "Label": "Time of up movement", "Value": 1500, "Units": "0.01 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 156, "Node": 37, "Genre": "Config", "Help": "This parameter determines the time needed for roller blinds to reach the top. For modes with positioning value is set automatically during calibration, otherwise, it must be set manually. 1-90000 (0.01 - 900.00s). Default setting: 6000 (60s).", "ValueIDKey": 43910096997842963, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370140} +OpenZWave/1/node/37/instance/1/commandclass/112/value/44191571974553619/,{ "Label": "Time of down movement", "Value": 1318, "Units": "0.01 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 157, "Node": 37, "Genre": "Config", "Help": "This parameter determines time needed for roller blinds to reach the bottom. For modes with positioning value is set automatically during calibration, otherwise, it must be set manually. 1-90000 (0.01 - 900.00s). Default setting: 6000 (60s).", "ValueIDKey": 44191571974553619, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370382} +OpenZWave/1/node/37/instance/1/commandclass/145/,{ "Instance": 1, "CommandClassId": 145, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "CommandClassVersion": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/145/value/627326993/,{ "Label": "Venetian Blind slat position", "Value": 0, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 627326993, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/145/value/281475604037649/,{ "Label": "Venetian blind tilt position", "Value": 0, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "Index": 1, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 281475604037649, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/38/value/2533275424358417/,{ "Label": "Instance 1: Target Value", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358417, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370645} +OpenZWave/1/node/37/instance/1/commandclass/38/value/1688850485837841/,{ "Label": "Instance 1: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837841, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/38/value/1970325462548504/,{ "Label": "Instance 1: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548504, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/38/value/2251800439259160/,{ "Label": "Instance 1: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259160, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/38/value/1407375517515793/,{ "Label": "Instance 1: Dimming Duration", "Value": 16, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515793, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370645} +OpenZWave/1/node/37/instance/1/commandclass/38/value/625573905/,{ "Label": "Instance 1: Level", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573905, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370642} +OpenZWave/1/node/37/instance/1/commandclass/38/value/281475602284568/,{ "Label": "Instance 1: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284568, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/38/value/562950578995224/,{ "Label": "Instance 1: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995224, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/38/value/844425564094480/,{ "Label": "Instance 1: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094480, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/38/value/1125900540805137/,{ "Label": "Instance 1: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805137, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "CommandClassVersion": 3, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/91/value/72057594664370195/,{ "Label": "Scene Count", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594664370195, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/91/value/72339069645275155/,{ "Label": "Scene Reset Timeout", "Value": 1000, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 257, "Node": 37, "Genre": "Config", "Help": "", "ValueIDKey": 72339069645275155, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/91/value/281475603152916/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" }, { "Value": 4, "Label": "Pressed 2 Times" }, { "Value": 5, "Label": "Pressed 3 Times" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 281475603152916, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/91/value/562950579863572/,{ "Label": "Scene 2", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" }, { "Value": 4, "Label": "Pressed 2 Times" }, { "Value": 5, "Label": "Pressed 3 Times" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579863572, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/94/value/634880017/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880017, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/94/value/281475611590678/,{ "Label": "Instance 1: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590678, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/94/value/562950588301334/,{ "Label": "Instance 1: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301334, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/114/value/635207699/,{ "Label": "Loaded Config Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 37, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 635207699, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/114/value/281475611918355/,{ "Label": "Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 37, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475611918355, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/114/value/562950588629011/,{ "Label": "Latest Available Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 37, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950588629011, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/114/value/844425565339671/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 37, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425565339671, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/114/value/1125900542050327/,{ "Label": "Serial Number", "Value": "0000000000000dd0", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 37, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900542050327, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/value/635224084/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 37, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 635224084, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} +OpenZWave/1/node/37/instance/1/commandclass/115/value/281475611934737/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 37, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475611934737, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} +OpenZWave/1/node/37/instance/1/commandclass/115/value/562950588645400/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 37, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950588645400, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/value/844425565356049/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425565356049, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1125900542066708/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900542066708, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1407375518777366/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375518777366, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1688850495488024/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 37, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850495488024, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1970325472198680/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 37, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325472198680, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/value/2251800448909332/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 37, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800448909332, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/115/value/2533275425619990/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275425619990, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "CommandClassVersion": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/117/value/635256852/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Unprotected", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 635256852, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} +OpenZWave/1/node/37/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/134/value/635535383/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 635535383, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/134/value/281475612246039/,{ "Label": "Protocol Version", "Value": "6.02", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 37, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475612246039, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/134/value/562950588956695/,{ "Label": "Application Version", "Value": "5.01", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 37, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950588956695, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/152/value/635830288/,{ "Label": "Instance 1: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830288, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/50/value/625770514/,{ "Label": "Electric - kWh", "Value": 0.0, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 625770514, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} +OpenZWave/1/node/37/instance/1/commandclass/50/value/562950579191826/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579191826, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} +OpenZWave/1/node/37/instance/1/commandclass/50/value/72057594663698448/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594663698448, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} +OpenZWave/1/node/37/instance/1/commandclass/50/value/72339069648797720/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 72339069648797720, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/113/value/72057594664730641/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730641, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/113/value/2251800440487956/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 6, "Label": "Over Current Detected" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 37, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800440487956, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/113/value/74872344431837207/,{ "Label": "Error Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 37, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344431837207, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/1/commandclass/113/value/2533275417198612/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 37, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275417198612, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/2/,{ "Instance": 2, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/2/commandclass/38/,{ "Instance": 2, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/38/value/2533275424358433/,{ "Label": "Instance 2: Target Value", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358433, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370142} +OpenZWave/1/node/37/instance/2/commandclass/38/value/1688850485837857/,{ "Label": "Instance 2: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837857, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/38/value/1970325462548520/,{ "Label": "Instance 2: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548520, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/38/value/2251800439259176/,{ "Label": "Instance 2: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259176, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/38/value/1407375517515809/,{ "Label": "Instance 2: Dimming Duration", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515809, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} +OpenZWave/1/node/37/instance/2/commandclass/38/value/625573921/,{ "Label": "Instance 2: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573921, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370142} +OpenZWave/1/node/37/instance/2/commandclass/38/value/281475602284584/,{ "Label": "Instance 2: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284584, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/38/value/562950578995240/,{ "Label": "Instance 2: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995240, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/38/value/844425564094496/,{ "Label": "Instance 2: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094496, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/38/value/1125900540805153/,{ "Label": "Instance 2: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805153, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/94/value/634880033/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880033, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/94/value/281475611590694/,{ "Label": "Instance 2: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590694, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/94/value/562950588301350/,{ "Label": "Instance 2: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301350, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/152/,{ "Instance": 2, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/2/commandclass/152/value/635830304/,{ "Label": "Instance 2: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830304, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/2/commandclass/50/,{ "Instance": 2, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/50/value/625770530/,{ "Label": "Electric - kWh", "Value": 0.0, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 625770530, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370035} +OpenZWave/1/node/37/instance/2/commandclass/50/value/562950579191842/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579191842, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370663} +OpenZWave/1/node/37/instance/2/commandclass/50/value/72057594663698464/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594663698464, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370035} +OpenZWave/1/node/37/instance/2/commandclass/50/value/72339069648797736/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 72339069648797736, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/2/commandclass/113/,{ "Instance": 2, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/2/commandclass/113/value/72057594664730657/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730657, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/2/commandclass/113/value/2251800440487972/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 6, "Label": "Over Current Detected" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 37, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800440487972, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/2/commandclass/113/value/74872344431837223/,{ "Label": "Error Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 37, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344431837223, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/2/commandclass/113/value/2533275417198628/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 37, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275417198628, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/3/,{ "Instance": 3, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/3/commandclass/38/,{ "Instance": 3, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/38/value/2533275424358449/,{ "Label": "Instance 3: Target Value", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358449, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} +OpenZWave/1/node/37/instance/3/commandclass/38/value/1688850485837873/,{ "Label": "Instance 3: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837873, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/38/value/1970325462548536/,{ "Label": "Instance 3: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548536, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/38/value/2251800439259192/,{ "Label": "Instance 3: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259192, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/38/value/1407375517515825/,{ "Label": "Instance 3: Dimming Duration", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515825, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} +OpenZWave/1/node/37/instance/3/commandclass/38/value/625573937/,{ "Label": "Instance 3: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573937, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} +OpenZWave/1/node/37/instance/3/commandclass/38/value/281475602284600/,{ "Label": "Instance 3: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284600, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/38/value/562950578995256/,{ "Label": "Instance 3: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995256, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/38/value/844425564094512/,{ "Label": "Instance 3: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094512, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/38/value/1125900540805169/,{ "Label": "Instance 3: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805169, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/94/,{ "Instance": 3, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/94/value/634880049/,{ "Label": "Instance 3: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880049, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/94/value/281475611590710/,{ "Label": "Instance 3: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590710, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/94/value/562950588301366/,{ "Label": "Instance 3: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301366, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} +OpenZWave/1/node/37/instance/3/commandclass/152/,{ "Instance": 3, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} +OpenZWave/1/node/37/instance/3/commandclass/152/value/635830320/,{ "Label": "Instance 3: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 3, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830320, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} +OpenZWave/1/node/37/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.1" ], "TimeStamp": 1593369823} +OpenZWave/1/node/37/association/2/,{ "Name": "Roller Shutter", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1593369913} +OpenZWave/1/node/37/association/3/,{ "Name": "Slats", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1593369913} From 333dccc7afd170bfc1a8902b8d75531a49d3754a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Jun 2020 15:01:30 +0200 Subject: [PATCH 073/428] Remove Hue configurator demo from demo integration (#37250) --- homeassistant/components/demo/__init__.py | 32 --------------------- homeassistant/components/demo/manifest.json | 2 +- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 8121d493315..afba3e2878a 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -1,7 +1,6 @@ """Set up the demo environment that mimics interaction with devices.""" import asyncio import logging -import time from homeassistant import bootstrap, config_entries from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START @@ -136,37 +135,6 @@ async def async_setup(hass, config): "This is an example of a persistent notification.", title="Example Notification" ) - # Set up configurator - configurator_ids = [] - configurator = hass.components.configurator - - def hue_configuration_callback(data): - """Fake callback, mark config as done.""" - time.sleep(2) - - # First time it is called, pretend it failed. - if len(configurator_ids) == 1: - configurator.notify_errors( - configurator_ids[0], "Failed to register, please try again." - ) - - configurator_ids.append(0) - else: - configurator.request_done(configurator_ids[0]) - - request_id = configurator.async_request_config( - "Philips Hue", - hue_configuration_callback, - description=( - "Press the button on the bridge to register Philips " - "Hue with Home Assistant." - ), - description_image="/static/images/config_philips_hue.jpg", - fields=[{"id": "username", "name": "Username"}], - submit_caption="I have pressed the button", - ) - configurator_ids.append(request_id) - async def demo_start_listener(_event): """Finish set up.""" await finish_setup(hass, config) diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 0abe5fb3347..697e6520d7d 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -2,7 +2,7 @@ "domain": "demo", "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", - "dependencies": ["conversation", "zone", "group", "configurator"], + "dependencies": ["conversation", "zone", "group"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } From 86c27b50f1282658b75fe71f9f6fd2fcabc2eaa2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jun 2020 16:35:10 +0200 Subject: [PATCH 074/428] Bump pychromecast to 7.0.1 (#37225) * Bump pychromecast to 7.0.1 * Fix tests * Mark configuration via platform for removal in 0.116 * Fix uuid check --- homeassistant/components/cast/discovery.py | 83 +++++++---- homeassistant/components/cast/helpers.py | 6 +- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 132 ++++++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/test_media_player.py | 130 ++++++++++++++--- 7 files changed, 213 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index e7d13ea0a18..9266709a83a 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -21,20 +21,16 @@ _LOGGER = logging.getLogger(__name__) def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo): """Discover a Chromecast.""" - if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: - _LOGGER.debug("Discovered previous chromecast %s", info) + if info.uuid is None: + _LOGGER.error("Discovered chromecast without uuid %s", info) + return - # Either discovered completely new chromecast or a "moved" one. - _LOGGER.debug("Discovered chromecast %s", info) + if info.uuid in hass.data[KNOWN_CHROMECAST_INFO_KEY]: + _LOGGER.debug("Discovered update for known chromecast %s", info) + else: + _LOGGER.debug("Discovered chromecast %s", info) - if info.uuid is not None: - # Remove previous cast infos with same uuid from known chromecasts. - same_uuid = { - x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid - } - hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid - - hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) + hass.data[KNOWN_CHROMECAST_INFO_KEY][info.uuid] = info dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) @@ -54,47 +50,72 @@ def setup_internal_discovery(hass: HomeAssistant) -> None: # Internal discovery is already running return - def internal_add_callback(name): - """Handle zeroconf discovery of a new chromecast.""" - mdns = listener.services[name] + def internal_add_update_callback(uuid, service_name): + """Handle zeroconf discovery of a new or updated chromecast.""" + service = listener.services[uuid] + + # For support of deprecated IP based white listing + zconf = ChromeCastZeroconf.get_zeroconf() + service_info = None + tries = 0 + while service_info is None and tries < 4: + try: + service_info = zconf.get_service_info( + "_googlecast._tcp.local.", service_name + ) + except OSError: + # If the zeroconf fails to receive the necessary data we abort + # adding the service + break + tries += 1 + + if not service_info: + _LOGGER.warning( + "setup_internal_discovery failed to get info for %s, %s", + uuid, + service_name, + ) + return + + addresses = service_info.parsed_addresses() + host = addresses[0] if addresses else service_info.server + discover_chromecast( hass, ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], + services=service[0], + uuid=service[1], + model_name=service[2], + friendly_name=service[3], + host=host, + port=service_info.port, ), ) - def internal_remove_callback(name, mdns): + def internal_remove_callback(uuid, service_name, service): """Handle zeroconf discovery of a removed chromecast.""" _remove_chromecast( hass, ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], + services=service[0], + uuid=service[1], + model_name=service[2], + friendly_name=service[3], ), ) _LOGGER.debug("Starting internal pychromecast discovery.") listener = pychromecast.CastListener( - internal_add_callback, + internal_add_update_callback, internal_remove_callback, - internal_add_callback, # Use internal_add_callback also for updates + internal_add_update_callback, ) browser = pychromecast.start_discovery(listener, ChromeCastZeroconf.get_zeroconf()) def stop_discovery(event): """Stop discovery of new chromecasts.""" _LOGGER.debug("Stopping internal pychromecast discovery.") - pychromecast.stop_discovery(browser) + pychromecast.discovery.stop_discovery(browser) hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 5a99d30f087..cc0ec911bbf 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -14,9 +14,9 @@ class ChromecastInfo: This also has the same attributes as the mDNS fields by zeroconf. """ - host = attr.ib(type=str) - port = attr.ib(type=int) - service = attr.ib(type=Optional[str], default=None) + services = attr.ib(type=Optional[set]) + host = attr.ib(type=Optional[str], default=None) + port = attr.ib(type=Optional[int], default=0) uuid = attr.ib( type=Optional[str], converter=attr.converters.optional(str), default=None ) # always convert UUID to string if not None diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index edf0373dd5d..b0d49681414 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==6.0.0"], + "requirements": ["pychromecast==7.0.1"], "after_dependencies": ["cloud","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 84917e0194a..3883c5ad725 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -55,7 +55,6 @@ from .const import ( DOMAIN as CAST_DOMAIN, KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, - SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, ) from .discovery import setup_internal_discovery @@ -64,6 +63,7 @@ from .helpers import CastStatusListener, ChromecastInfo, ChromeCastZeroconf _LOGGER = logging.getLogger(__name__) CONF_IGNORE_CEC = "ignore_cec" +CONF_UUID = "uuid" CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" SUPPORT_CAST = ( @@ -78,11 +78,26 @@ SUPPORT_CAST = ( ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list, [cv.string]), - } +ENTITY_SCHEMA = vol.All( + cv.deprecated(CONF_HOST, invalidation_version="0.116"), + vol.Schema( + { + vol.Exclusive(CONF_HOST, "device_identifier"): cv.string, + vol.Exclusive(CONF_UUID, "device_identifier"): cv.string, + vol.Optional(CONF_IGNORE_CEC): vol.All(cv.ensure_list, [cv.string]), + } + ), +) + +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HOST, invalidation_version="0.116"), + PLATFORM_SCHEMA.extend( + { + vol.Exclusive(CONF_HOST, "device_identifier"): cv.string, + vol.Exclusive(CONF_UUID, "device_identifier"): cv.string, + vol.Optional(CONF_IGNORE_CEC): vol.All(cv.ensure_list, [cv.string]), + } + ), ) @@ -111,13 +126,14 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): - """Set up thet Cast platform. + """Set up the Cast platform. Deprecated. """ _LOGGER.warning( "Setting configuration for Cast via platform is deprecated. " "Configure via Cast integration instead." + "This option will become invalid in version 0.116." ) await _async_setup_platform(hass, config, async_add_entities, discovery_info) @@ -130,7 +146,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # no pending task done, _ = await asyncio.wait( - [_async_setup_platform(hass, cfg, async_add_entities, None) for cfg in config] + [ + _async_setup_platform(hass, ENTITY_SCHEMA(cfg), async_add_entities, None) + for cfg in config + ] ) if any([task.exception() for task in done]): exceptions = [task.exception() for task in done] @@ -146,18 +165,25 @@ async def _async_setup_platform( # Import CEC IGNORE attributes pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) - hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, set()) + hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, dict()) info = None if discovery_info is not None: - info = ChromecastInfo(host=discovery_info["host"], port=discovery_info["port"]) + info = ChromecastInfo( + host=discovery_info["host"], port=discovery_info["port"], services=None + ) + elif CONF_UUID in config: + info = ChromecastInfo(uuid=config[CONF_UUID], services=None) elif CONF_HOST in config: - info = ChromecastInfo(host=config[CONF_HOST], port=DEFAULT_PORT) + info = ChromecastInfo(host=config[CONF_HOST], port=DEFAULT_PORT, services=None) @callback def async_cast_discovered(discover: ChromecastInfo) -> None: """Handle discovery of a new chromecast.""" - if info is not None and info.host_port != discover.host_port: + if info is not None and ( + (info.uuid is not None and info.uuid != discover.uuid) + or (info.host is not None and info.host_port != discover.host_port) + ): # Waiting for a specific cast device, this is not it. return @@ -168,7 +194,7 @@ async def _async_setup_platform( async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) # Re-play the callback for all past chromecasts, store the objects in # a list to avoid concurrent modification resulting in exception. - for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): + for chromecast in hass.data[KNOWN_CHROMECAST_INFO_KEY].values(): async_cast_discovered(chromecast) ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass)) @@ -187,10 +213,7 @@ class CastDevice(MediaPlayerEntity): """Initialize the cast device.""" self._cast_info = cast_info - self.services = None - if cast_info.service: - self.services = set() - self.services.add(cast_info.service) + self.services = cast_info.services self._chromecast: Optional[pychromecast.Chromecast] = None self.cast_status = None self.media_status = None @@ -211,9 +234,6 @@ class CastDevice(MediaPlayerEntity): self._add_remove_handler = async_dispatcher_connect( self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered ) - self._del_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed - ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.hass.async_create_task( async_create_catching_coro(self.async_set_cast_info(self._cast_info)) @@ -245,42 +265,26 @@ class CastDevice(MediaPlayerEntity): self._cast_info = cast_info - if self.services is not None: - if cast_info.service not in self.services: - _LOGGER.debug( - "[%s %s (%s:%s)] Got new service: %s (%s)", - self.entity_id, - self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, - cast_info.service, - self.services, - ) - - self.services.add(cast_info.service) - if self._chromecast is not None: # Only setup the chromecast once, added elements to services # will automatically be picked up. return _LOGGER.debug( - "[%s %s (%s:%s)] Connecting to cast device by service %s", + "[%s %s] Connecting to cast device by service %s", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, self.services, ) chromecast = await self.hass.async_add_executor_job( pychromecast.get_chromecast_from_service, ( self.services, - ChromeCastZeroconf.get_zeroconf(), cast_info.uuid, cast_info.model_name, cast_info.friendly_name, ), + ChromeCastZeroconf.get_zeroconf(), ) self._chromecast = chromecast @@ -296,30 +300,15 @@ class CastDevice(MediaPlayerEntity): self._chromecast.start() self.async_write_ha_state() - async def async_del_cast_info(self, cast_info): - """Remove the service.""" - self.services.discard(cast_info.service) - _LOGGER.debug( - "[%s %s (%s:%s)] Remove service: %s (%s)", - self.entity_id, - self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, - cast_info.service, - self.services, - ) - async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" if self._chromecast is None: # Can't disconnect if not connected. return _LOGGER.debug( - "[%s %s (%s:%s)] Disconnecting from chromecast socket.", + "[%s %s] Disconnecting from chromecast socket.", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, ) self._available = False self.async_write_ha_state() @@ -359,11 +348,9 @@ class CastDevice(MediaPlayerEntity): def new_connection_status(self, connection_status): """Handle updates of connection status.""" _LOGGER.debug( - "[%s %s (%s:%s)] Received cast device connection status: %s", + "[%s %s] Received cast device connection status: %s", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, connection_status.status, ) if connection_status.status == CONNECTION_STATUS_DISCONNECTED: @@ -378,11 +365,9 @@ class CastDevice(MediaPlayerEntity): # Only update state when availability changed to put less pressure # on state machine. _LOGGER.debug( - "[%s %s (%s:%s)] Cast device availability changed: %s", + "[%s %s] Cast device availability changed: %s", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, connection_status.status, ) self._available = new_available @@ -391,11 +376,9 @@ class CastDevice(MediaPlayerEntity): def multizone_new_media_status(self, group_uuid, media_status): """Handle updates of audio group media status.""" _LOGGER.debug( - "[%s %s (%s:%s)] Multizone %s media status: %s", + "[%s %s] Multizone %s media status: %s", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, group_uuid, media_status, ) @@ -738,32 +721,9 @@ class CastDevice(MediaPlayerEntity): # Discovered is not our device. return - if self.services is None: - _LOGGER.warning( - "[%s %s (%s:%s)] Received update for manually added Cast", - self.entity_id, - self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, - ) - return - _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) await self.async_set_cast_info(discover) - async def _async_cast_removed(self, discover: ChromecastInfo): - """Handle removal of Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - - if self._cast_info.uuid != discover.uuid: - # Removed is not our device. - return - - _LOGGER.debug("Removed chromecast with same UUID: %s", discover) - await self.async_del_cast_info(discover) - async def _async_stop(self, event): """Disconnect socket on Home Assistant stop.""" await self._async_disconnect() diff --git a/requirements_all.txt b/requirements_all.txt index 9c47005087e..227c4a2ba31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1255,7 +1255,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==6.0.0 +pychromecast==7.0.1 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4876770bdc..cbfa27cbcf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==6.0.0 +pychromecast==7.0.1 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 90b3896396c..9fd30357ea6 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -56,10 +56,24 @@ def get_fake_chromecast_info( ): """Generate a Fake ChromecastInfo with the specified arguments.""" return ChromecastInfo( - host=host, port=port, uuid=uuid, friendly_name="Speaker", service="the-service" + host=host, + port=port, + uuid=uuid, + friendly_name="Speaker", + services={"the-service"}, ) +def get_fake_zconf(host="192.168.178.42", port=8009): + """Generate a Fake Zeroconf object with the specified arguments.""" + parsed_addresses = MagicMock() + parsed_addresses.return_value = [host] + service_info = MagicMock(parsed_addresses=parsed_addresses, port=port) + zconf = MagicMock() + zconf.get_service_info.return_value = service_info + return zconf + + async def async_setup_cast(hass, config=None, discovery_info=None): """Set up the cast platform.""" if config is None: @@ -96,14 +110,13 @@ async def async_setup_cast_internal_discovery(hass, config=None, discovery_info= def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" - listener.services[service_name] = ( - info.host, - info.port, + listener.services[info.uuid] = ( + {service_name}, info.uuid, info.model_name, info.friendly_name, ) - discovery_callback(service_name) + discovery_callback(info.uuid, service_name) return discover_chromecast, add_entities @@ -113,6 +126,7 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas listener = MagicMock(services={}) browser = MagicMock(zc={}) chromecast = get_fake_chromecast(info) + zconf = get_fake_zconf(host=info.host, port=info.port) cast.CastStatusListener = MagicMock() @@ -125,6 +139,9 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas ) as cast_listener, patch( "homeassistant.components.cast.discovery.pychromecast.start_discovery", return_value=browser, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, ): await async_setup_component( hass, @@ -138,14 +155,13 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" - listener.services[service_name] = ( - info.host, - info.port, + listener.services[info.uuid] = ( + {service_name}, info.uuid, info.model_name, info.friendly_name, ) - discovery_callback(service_name) + discovery_callback(info.uuid, service_name) discover_chromecast("the-service", info) await hass.async_block_till_done() @@ -184,7 +200,7 @@ async def test_stop_discovery_called_on_stop(hass): assert start_discovery.call_count == 1 with patch( - "homeassistant.components.cast.discovery.pychromecast.stop_discovery" + "homeassistant.components.cast.discovery.pychromecast.discovery.stop_discovery" ) as stop_discovery: # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) @@ -229,15 +245,26 @@ async def test_replay_past_chromecasts(hass): cast_group2 = get_fake_chromecast_info( host="host2", port=42, uuid=UUID("9462202c-e747-4af5-a66b-7dce0e1ebc09") ) + zconf_1 = get_fake_zconf(host="host1", port=42) + zconf_2 = get_fake_zconf(host="host2", port=42) discover_cast, add_dev1 = await async_setup_cast_internal_discovery( hass, discovery_info={"host": "host1", "port": 42} ) - discover_cast("service2", cast_group2) - await hass.async_block_till_done() - assert add_dev1.call_count == 0 - discover_cast("service1", cast_group1) + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_2, + ): + discover_cast("service2", cast_group2) + await hass.async_block_till_done() + assert add_dev1.call_count == 0 + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service1", cast_group1) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 @@ -249,21 +276,61 @@ async def test_replay_past_chromecasts(hass): assert add_dev2.call_count == 1 -async def test_manual_cast_chromecasts(hass): +async def test_manual_cast_chromecasts_host(hass): """Test only wanted casts are added for manual configuration.""" cast_1 = get_fake_chromecast_info(host="configured_host") cast_2 = get_fake_chromecast_info(host="other_host", uuid=FakeUUID2) + zconf_1 = get_fake_zconf(host="configured_host") + zconf_2 = get_fake_zconf(host="other_host") # Manual configuration of media player with host "configured_host" discover_cast, add_dev1 = await async_setup_cast_internal_discovery( hass, config={"host": "configured_host"} ) - discover_cast("service2", cast_2) + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_2, + ): + discover_cast("service2", cast_2) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 0 - discover_cast("service1", cast_1) + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service1", cast_1) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + assert add_dev1.call_count == 1 + + +async def test_manual_cast_chromecasts_uuid(hass): + """Test only wanted casts are added for manual configuration.""" + cast_1 = get_fake_chromecast_info(host="host_1", uuid=FakeUUID) + cast_2 = get_fake_chromecast_info(host="host_2", uuid=FakeUUID2) + zconf_1 = get_fake_zconf(host="host_1") + zconf_2 = get_fake_zconf(host="host_2") + + # Manual configuration of media player with host "configured_host" + discover_cast, add_dev1 = await async_setup_cast_internal_discovery( + hass, config={"uuid": FakeUUID} + ) + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_2, + ): + discover_cast("service2", cast_2) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + assert add_dev1.call_count == 0 + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service1", cast_1) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 @@ -273,15 +340,25 @@ async def test_auto_cast_chromecasts(hass): """Test all discovered casts are added for default configuration.""" cast_1 = get_fake_chromecast_info(host="some_host") cast_2 = get_fake_chromecast_info(host="other_host", uuid=FakeUUID2) + zconf_1 = get_fake_zconf(host="some_host") + zconf_2 = get_fake_zconf(host="other_host") # Manual configuration of media player with host "configured_host" discover_cast, add_dev1 = await async_setup_cast_internal_discovery(hass) - discover_cast("service2", cast_2) + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service2", cast_2) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 - discover_cast("service1", cast_1) + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_2, + ): + discover_cast("service1", cast_1) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 2 @@ -291,15 +368,26 @@ async def test_update_cast_chromecasts(hass): """Test discovery of same UUID twice only adds one cast.""" cast_1 = get_fake_chromecast_info(host="old_host") cast_2 = get_fake_chromecast_info(host="new_host") + zconf_1 = get_fake_zconf(host="old_host") + zconf_2 = get_fake_zconf(host="new_host") # Manual configuration of media player with host "configured_host" discover_cast, add_dev1 = await async_setup_cast_internal_discovery(hass) - discover_cast("service1", cast_1) + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service1", cast_1) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 - discover_cast("service2", cast_2) + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_2, + ): + discover_cast("service2", cast_2) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 From b78f163bb0e50b6344ecdfa2a0d0da5c9fb48db3 Mon Sep 17 00:00:00 2001 From: mdegat01 Date: Tue, 30 Jun 2020 12:59:21 -0400 Subject: [PATCH 075/428] Changed FilterTest namedtuples to dataclasses (#37252) --- tests/components/apache_kafka/test_init.py | 23 ++++++++++++++++--- tests/components/azure_event_hub/test_init.py | 11 +++++++-- tests/components/google_pubsub/test_init.py | 13 +++++++---- tests/components/prometheus/test_init.py | 13 +++++++---- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py index 45791da082e..ea62bb0569d 100644 --- a/tests/components/apache_kafka/test_init.py +++ b/tests/components/apache_kafka/test_init.py @@ -1,5 +1,7 @@ """The tests for the Apache Kafka component.""" -from collections import namedtuple +from asyncio import AbstractEventLoop +from dataclasses import dataclass +from typing import Callable, Type import pytest @@ -16,8 +18,23 @@ MIN_CONFIG = { "port": 8080, "topic": "topic", } -FilterTest = namedtuple("FilterTest", "id should_pass") -MockKafkaClient = namedtuple("MockKafkaClient", "init start send_and_wait") + + +@dataclass +class FilterTest: + """Class for capturing a filter test.""" + + id: str + should_pass: bool + + +@dataclass +class MockKafkaClient: + """Mock of the Apache Kafka client for testing.""" + + init: Callable[[Type[AbstractEventLoop], str, str], None] + start: Callable[[], None] + send_and_wait: Callable[[str, str], None] @pytest.fixture(name="mock_client") diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index bec710c5f3c..42bfefbcb3c 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -1,5 +1,5 @@ """The tests for the Azure Event Hub component.""" -from collections import namedtuple +from dataclasses import dataclass import pytest @@ -17,7 +17,14 @@ MIN_CONFIG = { "event_hub_sas_policy": "policy", "event_hub_sas_key": "key", } -FilterTest = namedtuple("FilterTest", "id should_pass") + + +@dataclass +class FilterTest: + """Class for capturing a filter test.""" + + id: str + should_pass: bool @pytest.fixture(autouse=True, name="mock_client", scope="module") diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index aebcfa06b13..96be5b3ed62 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -1,5 +1,5 @@ """The tests for the Google Pub/Sub component.""" -from collections import namedtuple +from dataclasses import dataclass from datetime import datetime import pytest @@ -15,6 +15,14 @@ import tests.async_mock as mock GOOGLE_PUBSUB_PATH = "homeassistant.components.google_pubsub" +@dataclass +class FilterTest: + """Class for capturing a filter test.""" + + id: str + should_pass: bool + + async def test_datetime(): """Test datetime encoding.""" time = datetime(2019, 1, 13, 12, 30, 5) @@ -109,9 +117,6 @@ async def test_full_config(hass, mock_client): ) -FilterTest = namedtuple("FilterTest", "id should_pass") - - def make_event(entity_id): """Make a mock event for test.""" domain = split_entity_id(entity_id)[0] diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index ca86658a88f..7b5f3182595 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1,5 +1,5 @@ """The tests for the Prometheus exporter.""" -from collections import namedtuple +from dataclasses import dataclass import pytest @@ -22,6 +22,14 @@ import tests.async_mock as mock PROMETHEUS_PATH = "homeassistant.components.prometheus" +@dataclass +class FilterTest: + """Class for capturing a filter test.""" + + id: str + should_pass: bool + + @pytest.fixture async def prometheus_client(loop, hass, hass_client): """Initialize an hass_client with Prometheus component.""" @@ -202,9 +210,6 @@ async def test_full_config(hass, mock_client): assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0] -FilterTest = namedtuple("FilterTest", "id should_pass") - - def make_event(entity_id): """Make a mock event for test.""" domain = split_entity_id(entity_id)[0] From 38210ebbc6a06220b3be3a29259e13683b693f4e Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 30 Jun 2020 12:22:26 -0500 Subject: [PATCH 076/428] Enhance script integration to use new features in script helper (#37201) --- homeassistant/components/script/__init__.py | 143 ++++++++++++--- tests/components/script/test_init.py | 188 +++++++++++++++----- 2 files changed, 266 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index e80dcfa8027..740a5a21a5f 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import ( ATTR_NAME, CONF_ALIAS, CONF_ICON, + CONF_MODE, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF, @@ -21,7 +22,13 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.script import Script +from homeassistant.helpers.script import ( + DEFAULT_QUEUE_MAX, + SCRIPT_MODE_CHOICES, + SCRIPT_MODE_LEGACY, + SCRIPT_MODE_QUEUE, + Script, +) from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass @@ -37,11 +44,47 @@ CONF_DESCRIPTION = "description" CONF_EXAMPLE = "example" CONF_FIELDS = "fields" CONF_SEQUENCE = "sequence" +CONF_QUEUE_MAX = "queue_size" ENTITY_ID_FORMAT = DOMAIN + ".{}" EVENT_SCRIPT_STARTED = "script_started" + +def _deprecated_legacy_mode(config): + legacy_scripts = [] + for object_id, cfg in config.items(): + mode = cfg.get(CONF_MODE) + if mode is None: + legacy_scripts.append(object_id) + cfg[CONF_MODE] = SCRIPT_MODE_LEGACY + if legacy_scripts: + _LOGGER.warning( + "Script behavior has changed. " + "To continue using previous behavior, which is now deprecated, " + "add '%s: %s' to script(s): %s.", + CONF_MODE, + SCRIPT_MODE_LEGACY, + ", ".join(legacy_scripts), + ) + return config + + +def _queue_max(config): + for object_id, cfg in config.items(): + mode = cfg[CONF_MODE] + queue_max = cfg.get(CONF_QUEUE_MAX) + if mode == SCRIPT_MODE_QUEUE: + if queue_max is None: + cfg[CONF_QUEUE_MAX] = DEFAULT_QUEUE_MAX + elif queue_max is not None: + raise vol.Invalid( + f"{CONF_QUEUE_MAX} not valid with {mode} {CONF_MODE} " + f"for script '{object_id}'" + ) + return config + + SCRIPT_ENTRY_SCHEMA = vol.Schema( { vol.Optional(CONF_ALIAS): cv.string, @@ -54,11 +97,20 @@ SCRIPT_ENTRY_SCHEMA = vol.Schema( vol.Optional(CONF_EXAMPLE): cv.string, } }, + vol.Optional(CONF_MODE): vol.In(SCRIPT_MODE_CHOICES), + vol.Optional(CONF_QUEUE_MAX): vol.All(vol.Coerce(int), vol.Range(min=2)), } ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA)}, extra=vol.ALLOW_EXTRA + { + DOMAIN: vol.All( + cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA), + _deprecated_legacy_mode, + _queue_max, + ) + }, + extra=vol.ALLOW_EXTRA, ) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) @@ -91,7 +143,7 @@ def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: @callback def entities_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: - """Return all entities in a scene.""" + """Return all entities in script.""" if DOMAIN not in hass.data: return [] @@ -122,7 +174,7 @@ def scripts_with_device(hass: HomeAssistant, device_id: str) -> List[str]: @callback def devices_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: - """Return all devices in a scene.""" + """Return all devices in script.""" if DOMAIN not in hass.data: return [] @@ -152,13 +204,16 @@ async def async_setup(hass, config): async def turn_on_service(service): """Call a service to turn script on.""" - # We could turn on script directly here, but we only want to offer - # one way to do it. Otherwise no easy way to detect invocations. - var = service.data.get(ATTR_VARIABLES) - for script in await component.async_extract_from_service(service): - await hass.services.async_call( - DOMAIN, script.object_id, var, context=service.context - ) + variables = service.data.get(ATTR_VARIABLES) + for script_entity in await component.async_extract_from_service(service): + if script_entity.script.is_legacy: + await hass.services.async_call( + DOMAIN, script_entity.object_id, variables, context=service.context + ) + else: + await script_entity.async_turn_on( + variables=variables, context=service.context, wait=False + ) async def turn_off_service(service): """Cancel a script.""" @@ -172,8 +227,8 @@ async def async_setup(hass, config): async def toggle_service(service): """Toggle a script.""" - for script in await component.async_extract_from_service(service): - await script.async_toggle(context=service.context) + for script_entity in await component.async_extract_from_service(service): + await script_entity.async_toggle(context=service.context) hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service, schema=RELOAD_SERVICE_SCHEMA @@ -197,24 +252,40 @@ async def _async_process_config(hass, config, component): async def service_handler(service): """Execute a service call to script.